commit 43b894b60376f59b450302aa5768bae2dd35cb86 Author: dev Date: Mon Feb 16 22:26:09 2026 +0100 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ccbe46 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/node_modules/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/cpq-engine.iml b/.idea/cpq-engine.iml new file mode 100644 index 0000000..24643cc --- /dev/null +++ b/.idea/cpq-engine.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/dictionaries/project.xml b/.idea/dictionaries/project.xml new file mode 100644 index 0000000..2013773 --- /dev/null +++ b/.idea/dictionaries/project.xml @@ -0,0 +1,7 @@ + + + + hspot + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..30ba45e --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6d23e0e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1724 @@ +{ + "name": "cpq-engine", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cpq-engine", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@types/node": "^25.2.3", + "ts-node": "^10.9.2", + "tsx": "^4.21.0", + "typescript": "^5.9.3", + "vitest": "^4.0.18" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "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": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "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": { + "version": "25.2.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", + "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz", + "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "chai": "^6.2.1", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz", + "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.0.18", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz", + "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz", + "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.0.18", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz", + "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz", + "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz", + "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.0.18", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "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" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "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" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz", + "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.0.18", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz", + "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.0.18", + "@vitest/mocker": "4.0.18", + "@vitest/pretty-format": "4.0.18", + "@vitest/runner": "4.0.18", + "@vitest/snapshot": "4.0.18", + "@vitest/spy": "4.0.18", + "@vitest/utils": "4.0.18", + "es-module-lexer": "^1.7.0", + "expect-type": "^1.2.2", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^3.10.0", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.0.18", + "@vitest/browser-preview": "4.0.18", + "@vitest/browser-webdriverio": "4.0.18", + "@vitest/ui": "4.0.18", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..203e9ad --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "visual-configuration-engine", + "version": "2.0.0", + "description": "Sistema de configuración visual dinámica", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit", + "dev": "tsc --watch" + }, + "keywords": ["configuration", "visual", "typescript"], + "author": "ACTIVE THING", + "license": "EULA", + + + "devDependencies": { + "@types/node": "^25.2.3", + "ts-node": "^10.9.2", + "typescript": "^5.9.3", + "vitest": "^4.0.18" + } +} + diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..38f4b1c --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1,5 @@ +/** + * Constants - Public API + */ + +export * from './locales'; diff --git a/src/constants/locales.ts b/src/constants/locales.ts new file mode 100644 index 0000000..cac021a --- /dev/null +++ b/src/constants/locales.ts @@ -0,0 +1,87 @@ +/** + * ============================================================================ + * LOCALE CONSTANTS + * ============================================================================ + */ + +import type { SupportedLocale, LocaleInfo } from '@/types'; + +/** + * Locale por defecto del sistema + */ +export const DEFAULT_LOCALE: SupportedLocale = 'es'; + +/** + * Lista de todos los locales soportados + */ +export const SUPPORTED_LOCALES: readonly SupportedLocale[] = [ + 'es', + 'en', + 'de', + 'fr', + 'it', + 'pt', + 'ca', + 'eu', + 'gl' +] as const; + +/** + * Metadata de locales soportados + */ +export const LOCALE_INFO: Record = { + es: { + code: 'es', + name: 'Spanish', + nativeName: 'Español', + flag: '🇪🇸' + }, + en: { + code: 'en', + name: 'English', + nativeName: 'English', + flag: '🇬🇧' + }, + de: { + code: 'de', + name: 'German', + nativeName: 'Deutsch', + flag: '🇩🇪' + }, + fr: { + code: 'fr', + name: 'French', + nativeName: 'Français', + flag: '🇫🇷' + }, + it: { + code: 'it', + name: 'Italian', + nativeName: 'Italiano', + flag: '🇮🇹' + }, + pt: { + code: 'pt', + name: 'Portuguese', + nativeName: 'Português', + flag: '🇵🇹' + }, + ca: { + code: 'ca', + name: 'Catalan', + nativeName: 'Català', + flag: '🏴' + }, + eu: { + code: 'eu', + name: 'Basque', + nativeName: 'Euskara', + flag: '🏴' + }, + gl: { + code: 'gl', + name: 'Galician', + nativeName: 'Galego', + flag: '🏴' + } +}; diff --git a/src/engine/index.ts b/src/engine/index.ts new file mode 100644 index 0000000..d0a7fdf --- /dev/null +++ b/src/engine/index.ts @@ -0,0 +1,13 @@ +/** + * ============================================================================ + * ENGINE - Public API + * ============================================================================ + * + * Business logic del Visual Configuration Engine + */ + +// JSON Logic Engine +export * from './json-logic'; + +// Visual Configuration Engine +export * from './visual-config'; diff --git a/src/engine/json-logic/evaluator.ts b/src/engine/json-logic/evaluator.ts new file mode 100644 index 0000000..5373f1a --- /dev/null +++ b/src/engine/json-logic/evaluator.ts @@ -0,0 +1,130 @@ +/** + * ============================================================================ + * JSON LOGIC EVALUATOR + * ============================================================================ + */ + +import type { JsonLogic } from '@/types'; + +/** + * Evaluador de expresiones JsonLogic + */ +export class JsonLogicEvaluator { + /** + * Evalúa una expresión JsonLogic + */ + evaluate(expression: JsonLogic, data: Record): any { + // Primitivos + if (expression === null) return null; + if (typeof expression === 'boolean') return expression; + if (typeof expression === 'number') return expression; + if (typeof expression === 'string') return expression; + if (Array.isArray(expression)) { + return expression.map(item => this.evaluate(item, data)); + } + + if (typeof expression !== 'object') return expression; + + // Operadores + const expr = expression as Record; + const operator = Object.keys(expr)[0]; + const args = expr[operator]; + + switch (operator) { + // Variables + case 'var': + return this.getVar(args, data); + + // Lógicos + case '==': + case '===': + return this.evaluateAll(args, data).every((v, i, arr) => + i === 0 || v === arr[0] + ); + + case '!=': + case '!==': + return this.evaluate(args[0], data) !== this.evaluate(args[1], data); + + case '!': + return !this.evaluate(args, data); + + case 'and': + return this.evaluateAll(args, data).every(Boolean); + + case 'or': + return this.evaluateAll(args, data).some(Boolean); + + // Comparación + case '>': + return this.evaluate(args[0], data) > this.evaluate(args[1], data); + + case '>=': + return this.evaluate(args[0], data) >= this.evaluate(args[1], data); + + case '<': + return this.evaluate(args[0], data) < this.evaluate(args[1], data); + + case '<=': + return this.evaluate(args[0], data) <= this.evaluate(args[1], data); + + // Condicionales + case 'if': + const [condition, thenBranch, elseBranch] = args; + return this.evaluate(condition, data) + ? this.evaluate(thenBranch, data) + : this.evaluate(elseBranch !== undefined ? elseBranch : null, data); + + // Arrays + case 'in': + const [item, array] = args; + const evaluatedItem = this.evaluate(item, data); + const evaluatedArray = this.evaluate(array, data); + return Array.isArray(evaluatedArray) && evaluatedArray.includes(evaluatedItem); + + // Matemáticos + case '+': + return this.evaluateAll(args, data).reduce((a, b) => a + b, 0); + + case '-': + const nums = this.evaluateAll(args, data); + return nums.length === 1 ? -nums[0] : nums[0] - nums[1]; + + case '*': + return this.evaluateAll(args, data).reduce((a, b) => a * b, 1); + + case '/': + const [dividend, divisor] = this.evaluateAll(args, data); + return divisor !== 0 ? dividend / divisor : null; + + case '%': + return this.evaluate(args[0], data) % this.evaluate(args[1], data); + + default: + console.warn(`[JsonLogic] Unknown operator: ${operator}`); + return null; + } + } + + private evaluateAll(args: any[], data: Record): any[] { + return (Array.isArray(args) ? args : [args]).map(arg => + this.evaluate(arg, data) + ); + } + + private getVar(path: string | any, data: Record): any { + if (typeof path !== 'string') { + path = this.evaluate(path, data); + } + + const parts = String(path).split('.'); + let current: any = data; + + for (const part of parts) { + if (current === undefined || current === null) return undefined; + current = current[part]; + } + + return current; + } +} \ No newline at end of file diff --git a/src/engine/json-logic/index.ts b/src/engine/json-logic/index.ts new file mode 100644 index 0000000..68bc8fc --- /dev/null +++ b/src/engine/json-logic/index.ts @@ -0,0 +1,5 @@ +/** + * JSON Logic Engine - Public API + */ + +export * from './evaluator'; diff --git a/src/engine/visual-config/async-generator.ts b/src/engine/visual-config/async-generator.ts new file mode 100644 index 0000000..067c106 --- /dev/null +++ b/src/engine/visual-config/async-generator.ts @@ -0,0 +1,56 @@ +/** + * ============================================================================ + * ASYNC IMAGE GENERATOR + * ============================================================================ + */ + +import type { Section, AttrID, Value } from '@/types'; +import { generateSectionImageUrl } from '@/engine'; + +/** + * Generación async de imagen via API + */ +export async function generateSectionImageAsync( + section: Section, + globalAttributes: Record, + sectionAttributes: Record +): Promise { + const config = section.visualConfig; + const fallback = config.fallbackImage || '/images/fallback.jpg'; + + if (config.strategy !== 'api_generated' || !config.apiConfig) { + return generateSectionImageUrl(section, globalAttributes, sectionAttributes); + } + + try { + const params: Record = {}; + config.apiConfig.params?.forEach(attrId => { + const value = globalAttributes[attrId] ?? sectionAttributes[attrId]; + if (value !== undefined) params[attrId] = value; + }); + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), config.apiConfig.timeout || 5000); + + const response = await fetch(config.apiConfig.endpoint, { + method: config.apiConfig.method || 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sectionId: section.id, attributes: params }), + signal: controller.signal + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + console.error(`[Render] API ${config.apiConfig.endpoint} returned ${response.status}`); + return fallback; + } + + const data = await response.json(); + return data.imageUrl || fallback; + + } catch (error) { + console.error(`[Render] Network or Timeout error for ${section.id}:`, error); + return fallback; + } +} diff --git a/src/engine/visual-config/cache.ts b/src/engine/visual-config/cache.ts new file mode 100644 index 0000000..b4fcdd6 --- /dev/null +++ b/src/engine/visual-config/cache.ts @@ -0,0 +1,54 @@ +/** + * ============================================================================ + * ATTRIBUTE CACHE + * ============================================================================ + */ +import type { Attribute, ID } from '@/types'; + + +/** + * Cache para utilities del engine + */ +export class AttributeCache { + private uiVisibleCache = new Map, boolean>(); + private visualAttributesCache: Attribute[] | null = null; + private priceAttributesCache: Attribute[] | null = null; + + invalidate(): void { + this.uiVisibleCache.clear(); + this.visualAttributesCache = null; + this.priceAttributesCache = null; + } + + getUIVisible( + attrId: ID<'attr'>, + compute: () => boolean + ): boolean { + if (this.uiVisibleCache.has(attrId)) { + return this.uiVisibleCache.get(attrId)!; + } + const result = compute(); + this.uiVisibleCache.set(attrId, result); + return result; + } + + getVisualAttributes( + compute: () => Attribute[] + ): Attribute[] { + if (this.visualAttributesCache) { + return this.visualAttributesCache; + } + this.visualAttributesCache = compute(); + return this.visualAttributesCache; + } + + getPriceAttributes( + compute: () => Attribute[] + ): Attribute[] { + if (this.priceAttributesCache) { + return this.priceAttributesCache; + } + this.priceAttributesCache = compute(); + return this.priceAttributesCache; + } +} diff --git a/src/engine/visual-config/display-evaluator.ts b/src/engine/visual-config/display-evaluator.ts new file mode 100644 index 0000000..d45b1c8 --- /dev/null +++ b/src/engine/visual-config/display-evaluator.ts @@ -0,0 +1,34 @@ +/** + * ============================================================================ + * DISPLAY FLAG EVALUATOR + * ============================================================================ + */ + +import type {JsonLogic, ID, Value, AttrID} from '@/types'; +import type { JsonLogicEvaluator } from '@/engine'; +import { extractIdPart } from '@/utils'; + +/** + * Evalúa display flags con soporte para condicionales + */ +export function evaluateDisplayFlag( + flag: boolean | JsonLogic | undefined, + defaultValue: boolean, + attributeValues: Record, + evaluator: JsonLogicEvaluator +): boolean { + if (flag === undefined) return defaultValue; + if (typeof flag === 'boolean') return flag; + + // Es JsonLogic: evaluar + const data = { + attributes: Object.fromEntries( + (Object.entries(attributeValues) as [AttrID, Value][]).map(([id, value]) => [ + extractIdPart(id, 'at'), + value + ]) + ) + }; + + return evaluator.evaluate(flag, data); +} diff --git a/src/engine/visual-config/image-generator.ts b/src/engine/visual-config/image-generator.ts new file mode 100644 index 0000000..0296a4d --- /dev/null +++ b/src/engine/visual-config/image-generator.ts @@ -0,0 +1,38 @@ +/** + * ============================================================================ + * IMAGE GENERATOR + * ============================================================================ + */ + +import type { Section, AttrID, Value } from '@/types'; +import { buildSectionImageUrl } from '@/utils'; + +/** + * Genera URL de imagen para una sección + * Utiliza el URL builder centralizado + */ +export function generateSectionImageUrl( + section: Section, + globalAttributes: Record, + sectionAttributes: Record +): string { + const config = section.visualConfig; + + if (!config.imageUrlTemplate) { + console.warn(`[Render] No imageUrlTemplate for section ${section.id}`); + return config.fallbackImage || '/images/fallback.jpg'; + } + + return buildSectionImageUrl( + config.imageUrlTemplate, + section.id, + { + basePath: config.basePath, + fallbackImage: config.fallbackImage, + globalAttributeDependencies: config.globalAttributeDependencies, + sectionAttributeDependencies: config.sectionAttributeDependencies + }, + globalAttributes, + sectionAttributes + ); +} diff --git a/src/engine/visual-config/index.ts b/src/engine/visual-config/index.ts new file mode 100644 index 0000000..b0addc7 --- /dev/null +++ b/src/engine/visual-config/index.ts @@ -0,0 +1,9 @@ +/** + * Visual Configuration Engine - Public API + */ + +export * from './display-evaluator'; +export * from './image-generator'; +export * from './async-generator'; +export * from './rerender-detector'; +export * from './cache'; diff --git a/src/engine/visual-config/rerender-detector.ts b/src/engine/visual-config/rerender-detector.ts new file mode 100644 index 0000000..3f8a50c --- /dev/null +++ b/src/engine/visual-config/rerender-detector.ts @@ -0,0 +1,31 @@ +/** + * ============================================================================ + * RERENDER DETECTOR + * ============================================================================ + */ + +import type { Section, AttrID , Value } from '@/types'; +import { deepEqual } from '@/utils'; + +/** + * Determina si una sección debe re-renderizarse + */ +export function shouldRerenderSection( + section: Section, + changedAttributeId: AttrID, + oldValue: Value, + newValue: Value, + isGlobalAttribute: boolean +): boolean { + const config = section.visualConfig; + + // Verificar si el atributo afecta a esta sección + const affectsThisSection = isGlobalAttribute + ? config.globalAttributeDependencies?.includes(changedAttributeId) + : config.sectionAttributeDependencies?.includes(changedAttributeId); + + if (!affectsThisSection) return false; + + // Deep equality para valores complejos + return !deepEqual(oldValue, newValue); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..1d69be3 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,21 @@ +/** + * ============================================================================ + * VISUAL CONFIGURATION ENGINE - Public API + * ============================================================================ + * + * Sistema completo de configuración visual dinámica + * + * @packageDocumentation + */ + +// Types (re-export all types) +export * from './types'; + +// Constants +export * from './constants'; + +// Utilities +export * from './utils'; + +// Engine (Business Logic) +export * from './engine'; diff --git a/src/messages/index.ts b/src/messages/index.ts new file mode 100644 index 0000000..a18e185 --- /dev/null +++ b/src/messages/index.ts @@ -0,0 +1,6 @@ +// index.ts + + +export * from './messages'; +export * from './logger'; + diff --git a/src/messages/logger.ts b/src/messages/logger.ts new file mode 100644 index 0000000..a6e9c61 --- /dev/null +++ b/src/messages/logger.ts @@ -0,0 +1,233 @@ +/** + * ============================================================================ + * LOGGING SYSTEM - Structured Logging + * ============================================================================ + */ + +import type { I18nString, SupportedLocale } from '@/types'; +import { translate } from '@/utils'; +import type { MessageCategory } from './messages'; + +// ============================================================================ +// LOG LEVELS +// ============================================================================ + +export enum LogLevel { + DEBUG = 0, + INFO = 1, + WARN = 2, + ERROR = 3, + NONE = 4 +} + +// ============================================================================ +// LOG ENTRY +// ============================================================================ + +export interface LogEntry { + timestamp : Date; + level : LogLevel; + category : MessageCategory; + message : I18nString; + context? : Record; + locale? : SupportedLocale; +} + +// ============================================================================ +// LOGGER +// ============================================================================ + +export class Logger { + private static instance: Logger; + private logs : LogEntry[] = []; + private level : LogLevel = LogLevel.WARN; + private locale : SupportedLocale = 'es'; + private maxLogs : number = 1000; + + private constructor() {} + + static getInstance(): Logger { + if (!Logger.instance) { + Logger.instance = new Logger(); + } + return Logger.instance; + } + + /** + * Configura el nivel de logging + */ + setLevel(level: LogLevel): void { + this.level = level; + } + + /** + * Configura el locale para los mensajes + */ + setLocale(locale: SupportedLocale): void { + this.locale = locale; + } + + /** + * Configura el número máximo de logs a retener + */ + setMaxLogs(max: number): void { + this.maxLogs = max; + } + + /** + * Log nivel DEBUG + */ + debug(category: MessageCategory, message: I18nString, context?: Record): void { + this.log(LogLevel.DEBUG, category, message, context); + } + + /** + * Log nivel INFO + */ + info(category: MessageCategory, message: I18nString, context?: Record): void { + this.log(LogLevel.INFO, category, message, context); + } + + /** + * Log nivel WARN + */ + warn(category: MessageCategory, message: I18nString, context?: Record): void { + this.log(LogLevel.WARN, category, message, context); + } + + /** + * Log nivel ERROR + */ + error(category: MessageCategory, message: I18nString, context?: Record): void { + this.log(LogLevel.ERROR, category, message, context); + } + + /** + * Log interno + */ + private log( + level : LogLevel, + category: MessageCategory, + message : I18nString, + context?: Record + ): void { + // Skip si el nivel es menor al configurado + if (level < this.level) return; + + const entry: LogEntry = { + timestamp: new Date(), + level, + category, + message, + context, + locale: this.locale + }; + + // Añadir al historial + this.logs.push(entry); + + // Limitar tamaño del historial + if (this.logs.length > this.maxLogs) { + this.logs.shift(); + } + + // Output a consola + this.outputToConsole(entry); + } + + /** + * Output formateado a consola + */ + private outputToConsole(entry: LogEntry): void { + const translatedMessage = translate(entry.message, entry.locale || this.locale); + const prefix = `[${entry.category}]`; + const timestamp = entry.timestamp.toISOString(); + + switch (entry.level) { + case LogLevel.DEBUG: + console.debug(timestamp, prefix, translatedMessage, entry.context || ''); + break; + case LogLevel.INFO: + console.info(timestamp, prefix, translatedMessage, entry.context || ''); + break; + case LogLevel.WARN: + console.warn(timestamp, prefix, translatedMessage, entry.context || ''); + break; + case LogLevel.ERROR: + console.error(timestamp, prefix, translatedMessage, entry.context || ''); + break; + } + } + + /** + * Obtiene los logs filtrados + */ + getLogs(filters?: { + level?: LogLevel; + category?: MessageCategory; + since?: Date; + }): LogEntry[] { + let filtered = this.logs; + + if (filters?.level !== undefined) { + filtered = filtered.filter(log => log.level === filters.level); + } + + if (filters?.category) { + filtered = filtered.filter(log => log.category === filters.category); + } + + if (filters?.since) { + filtered = filtered.filter(log => log.timestamp >= filters.since!); + } + + return filtered; + } + + /** + * Limpia todos los logs + */ + clear(): void { + this.logs = []; + } + + /** + * Exporta logs como JSON + */ + export(): string { + return JSON.stringify(this.logs, null, 2); + } +} + +// ============================================================================ +// INSTANCIA SINGLETON EXPORTADA +// ============================================================================ + +export const logger = Logger.getInstance(); + +// ============================================================================ +// HELPERS +// ============================================================================ + +/** + * Configuración rápida para desarrollo + */ +export function setupDevelopmentLogging(): void { + logger.setLevel(LogLevel.DEBUG); + logger.setMaxLogs(5000); +} + +/** + * Configuración para producción + */ +export function setupProductionLogging(): void { + logger.setLevel(LogLevel.ERROR); + logger.setMaxLogs(500); +} + +/** + * Configuración para tests + */ +export function setupTestLogging(): void { + logger.setLevel(LogLevel.NONE); +} \ No newline at end of file diff --git a/src/messages/messages.ts b/src/messages/messages.ts new file mode 100644 index 0000000..b7cdbc2 --- /dev/null +++ b/src/messages/messages.ts @@ -0,0 +1,197 @@ +// messages.ts + +/** + * ============================================================================ + * MESSAGES SYSTEM - Error & Warning Messages + * ============================================================================ + * + * Sistema centralizado de mensajes con soporte i18n + */ + +import type { I18nString, AttrID, SectionID } from '@/types'; + +// ============================================================================ +// ERROR MESSAGES +// ============================================================================ + +export const ERRORS = { + // ID Errors + INVALID_ID_FORMAT: (id: string): I18nString => ({ + es: `Formato de ID inválido: "${id}". Formato esperado: "prefijo:valor"`, + en: `Invalid ID format: "${id}". Expected format: "prefix:value"` + }), + + ID_PREFIX_MISMATCH: (id: string, expected: string, got: string): I18nString => ({ + es: `Error de prefijo: se esperaba "${expected}:", se obtuvo "${got}:" en ID "${id}"`, + en: `Prefix mismatch: expected "${expected}:", got "${got}:" in ID "${id}"` + }), + + EMPTY_ID_VALUE: (id: string): I18nString => ({ + es: `Parte del ID vacía en: "${id}"`, + en: `Empty ID part in: "${id}"` + }), + + INVALID_ID_VALUE: (value: string): I18nString => ({ + es: `Valor de ID inválido: "${value}". No debe estar vacío o contener ":"`, + en: `Invalid ID value: "${value}". Must not be empty or contain ":"` + }), + + // I18n Errors + INVALID_I18N_STRING: (locale: string): I18nString => ({ + es: `I18nString inválido. Debe ser un string o un objeto con al menos la clave '${locale}'`, + en: `Invalid I18nString. Must be a string or object with at least '${locale}' key` + }), + + MISSING_DEFAULT_TRANSLATION: (locale: string): I18nString => ({ + es: `Falta la traducción obligatoria en '${locale}'`, + en: `Missing required translation for '${locale}'` + }), + + // Attribute Errors + COMPUTED_NOT_READONLY: (attrId: AttrID): I18nString => ({ + es: `Atributo calculado ${attrId} debe ser de solo lectura`, + en: `Computed attribute ${attrId} should be readonly` + }), + + COMPUTED_NO_DEPENDENCIES: (attrId: AttrID): I18nString => ({ + es: `Atributo calculado ${attrId} no tiene dependencias`, + en: `Computed attribute ${attrId} has no dependencies` + }), + + // Section Errors + NO_IMAGE_TEMPLATE: (sectionId: SectionID): I18nString => ({ + es: `No hay imageUrlTemplate para la sección ${sectionId}`, + en: `No imageUrlTemplate for section ${sectionId}` + }), + + UNRESOLVED_PLACEHOLDERS: (placeholders: string[], template: string): I18nString => ({ + es: `Placeholders sin resolver: ${placeholders.join(', ')} en template: ${template}`, + en: `Unresolved placeholders: ${placeholders.join(', ')} in template: ${template}` + }), + + PLACEHOLDER_NOT_IN_DEPENDENCIES: (placeholder: string, sectionId: SectionID): I18nString => ({ + es: `Placeholder ${placeholder} no está en las dependencias de ${sectionId}`, + en: `Placeholder ${placeholder} not in dependencies for ${sectionId}` + }), + + // API Errors + API_REQUEST_FAILED: (endpoint: string, status: number): I18nString => ({ + es: `La API ${endpoint} retornó código ${status}`, + en: `API ${endpoint} returned status ${status}` + }), + + API_TIMEOUT: (endpoint: string, timeout: number): I18nString => ({ + es: `Timeout de ${timeout}ms alcanzado para ${endpoint}`, + en: `Timeout of ${timeout}ms reached for ${endpoint}` + }), + + NETWORK_ERROR: (sectionId: SectionID): I18nString => ({ + es: `Error de red o timeout para la sección ${sectionId}`, + en: `Network or timeout error for section ${sectionId}` + }), + + // JsonLogic Errors + UNKNOWN_OPERATOR: (operator: string): I18nString => ({ + es: `Operador desconocido: ${operator}`, + en: `Unknown operator: ${operator}` + }) +} as const; + +// ============================================================================ +// WARNING MESSAGES +// ============================================================================ + +export const WARNINGS = { + // Attribute Warnings + FIXED_AFFECTS_PRICE: (attrId: AttrID): I18nString => ({ + es: `Atributo fijo ${attrId} no debería afectar al precio`, + en: `Fixed attribute ${attrId} should not affect price` + }), + + // Rendering Warnings + MISSING_GLOBAL_ATTRIBUTE: (attrId: AttrID): I18nString => ({ + es: `Falta atributo global: ${attrId}, usando 'default'`, + en: `Missing global attribute: ${attrId}, using 'default'` + }), + + MISSING_SECTION_ATTRIBUTE: (attrId: AttrID): I18nString => ({ + es: `Falta atributo de sección: ${attrId}, usando 'default'`, + en: `Missing section attribute: ${attrId}, using 'default'` + }), + + INVALID_SECTION_ID_FORMAT: (sectionId: SectionID): I18nString => ({ + es: `Formato de ID de sección inválido: ${sectionId}`, + en: `Invalid section ID format: ${sectionId}` + }), + + INVALID_VIEW_ID_FORMAT: (viewId: string): I18nString => ({ + es: `Formato de ID de vista inválido: ${viewId}`, + en: `Invalid view ID format: ${viewId}` + }) +} as const; + +// ============================================================================ +// VALIDATION MESSAGES +// ============================================================================ + +export const VALIDATION = { + REQUIRED_FIELD: (fieldName: I18nString): I18nString => ({ + es: `El campo ${typeof fieldName === 'string' ? fieldName : fieldName.es} es obligatorio`, + en: `Field ${typeof fieldName === 'string' ? fieldName : fieldName.en} is required` + }), + + INVALID_VALUE: (fieldName: I18nString, value: any): I18nString => ({ + es: `Valor inválido para ${typeof fieldName === 'string' ? fieldName : fieldName.es}: ${value}`, + en: `Invalid value for ${typeof fieldName === 'string' ? fieldName : fieldName.en}: ${value}` + }), + + OUT_OF_RANGE: (min: number, max: number): I18nString => ({ + es: `El valor debe estar entre ${min} y ${max}`, + en: `Value must be between ${min} and ${max}` + }), + + INVALID_FORMAT: (format: string): I18nString => ({ + es: `Formato inválido. Se esperaba: ${format}`, + en: `Invalid format. Expected: ${format}` + }) +} as const; + +// ============================================================================ +// INFO MESSAGES +// ============================================================================ + +export const INFO = { + USING_FALLBACK_IMAGE: (sectionId: SectionID): I18nString => ({ + es: `Usando imagen de fallback para ${sectionId}`, + en: `Using fallback image for ${sectionId}` + }), + + CACHE_HIT: (key: string): I18nString => ({ + es: `Cache hit para: ${key}`, + en: `Cache hit for: ${key}` + }), + + CACHE_MISS: (key: string): I18nString => ({ + es: `Cache miss para: ${key}`, + en: `Cache miss for: ${key}` + }) +} as const; + +// ============================================================================ +// MESSAGE CATEGORIES (para logging estructurado) +// ============================================================================ + +export const MESSAGE_CATEGORIES = { + ID : 'ID', + I18N : 'I18N', + ATTRIBUTE : 'ATTRIBUTE', + SECTION : 'SECTION', + RENDER : 'RENDER', + API : 'API', + JSON_LOGIC : 'JSON_LOGIC', + VALIDATION : 'VALIDATION', + CACHE : 'CACHE' +} as const; + + +export type MessageCategory = typeof MESSAGE_CATEGORIES[keyof typeof MESSAGE_CATEGORIES]; \ No newline at end of file diff --git a/src/types/core/api.types.ts b/src/types/core/api.types.ts new file mode 100644 index 0000000..0f456a6 --- /dev/null +++ b/src/types/core/api.types.ts @@ -0,0 +1,19 @@ +// api.types.ts +import type {AttrID} from "@/types"; + + + + +export type APIMethod = + 'GET' | + 'POST' + + +export interface APIConfig { + endpoint : string; + method? : APIMethod; + params? : string; + timeout? : number; +}; + + diff --git a/src/types/core/index.ts b/src/types/core/index.ts new file mode 100644 index 0000000..d68c9ba --- /dev/null +++ b/src/types/core/index.ts @@ -0,0 +1,15 @@ +/** + * Core Types - Public API + */ + +export * from './primitives.types'; +export * from './metadata.types'; +export * from './pricing.types.ts'; +export * from './media.types.ts'; +export * from './severity.types.ts'; +export * from './api.types.ts'; + + +// Re-export specific branded ID types for convenience +export type { ID, Value, DataType } from './primitives.types'; +export type { Metadata } from './metadata.types.ts'; diff --git a/src/types/core/media.types.ts b/src/types/core/media.types.ts new file mode 100644 index 0000000..d004824 --- /dev/null +++ b/src/types/core/media.types.ts @@ -0,0 +1,10 @@ +// media.types.ts + + + +export interface Media { + thumbnail? : string; + image? : string; + icon? : string; + gallery? : string[]; +} \ No newline at end of file diff --git a/src/types/core/metadata.types.ts b/src/types/core/metadata.types.ts new file mode 100644 index 0000000..57e0b0d --- /dev/null +++ b/src/types/core/metadata.types.ts @@ -0,0 +1,12 @@ +/** + * ============================================================================ + * METADATA - Extended Information + * ============================================================================ + */ + +/** + * Metadata flexible para extender cualquier entidad + */ + + +export type Metadata = Record; diff --git a/src/types/core/pricing.types.ts b/src/types/core/pricing.types.ts new file mode 100644 index 0000000..45d6da8 --- /dev/null +++ b/src/types/core/pricing.types.ts @@ -0,0 +1,17 @@ +// pricing.types.ts +/** + * ============================================================================ + * PRICING TYPES + * ============================================================================ + */ + +import type { JsonLogic } from '@/types'; + +/** + * Información de precio + */ +export interface Pricing { + baseAmount : number | 'consultation'; + currency? : string; + dynamicExpression?: JsonLogic; +} \ No newline at end of file diff --git a/src/types/core/primitives.types.ts b/src/types/core/primitives.types.ts new file mode 100644 index 0000000..8f4c4e3 --- /dev/null +++ b/src/types/core/primitives.types.ts @@ -0,0 +1,40 @@ +/** + * ============================================================================ + * CORE PRIMITIVES - Base Types + * ============================================================================ + * + * Tipos fundamentales del sistema + */ + +/** + * ID con tipo branded para type-safety + * + * @example + * const attrId: ID<'at'> = "at:suelo" + * const sectionId: ID<'sc'> = "sc:salon" + */ +export type ID = `${T}:${string}`; + +/** + * Valores que pueden tomar los atributos + */ +export type Value = + | string + | number + | boolean + | Date + | ID + | Value[]; + +/** + * Tipos de datos soportados + */ +export type DataType = + | 'string' + | 'number' + | 'boolean' + | 'date' + | 'currency' + | 'dimension' + | 'reference' + | 'list'; diff --git a/src/types/core/severity.types.ts b/src/types/core/severity.types.ts new file mode 100644 index 0000000..9d93d6d --- /dev/null +++ b/src/types/core/severity.types.ts @@ -0,0 +1,8 @@ +// severity.types.ts + + +export type Severity = + 'error' | + 'warning' | + 'info'; + diff --git a/src/types/i18n/i18n.types.ts b/src/types/i18n/i18n.types.ts new file mode 100644 index 0000000..f770225 --- /dev/null +++ b/src/types/i18n/i18n.types.ts @@ -0,0 +1,60 @@ +/** + * ============================================================================ + * INTERNATIONALIZATION TYPES + * ============================================================================ + */ + +/** + * Locales soportados por el sistema + */ +export type SupportedLocale = + | 'es' // Español + | 'en' // English + | 'de' // Deutsch + | 'fr' // Français + | 'it' // Italiano + | 'pt' // Português + | 'ca' // Català + | 'eu' // Euskara + | 'gl'; // Galego + +/** + * Objeto con traducciones tipadas + * Garantiza que al menos existe el locale por defecto (español) + */ +export type I18nTranslations = { + [K in SupportedLocale]?: string; +} & { + es: string; // Español es obligatorio +}; + +/** + * String internacionalizable + * + * Puede ser: + * - `string`: Texto simple (mismo en todos los idiomas) + * - `I18nTranslations`: Objeto con traducciones por locale + * + * @example + * // Simple (mismo en todos los idiomas) + * const label: I18nString = "OK"; + * + * // Con traducciones + * const greeting: I18nString = { + * es: "Hola", + * en: "Hello", + * de: "Hallo" + * }; + */ +export type I18nString = string | I18nTranslations; + +/** + * Información sobre un locale + */ +export interface LocaleInfo { + code : SupportedLocale; + name : string; + nativeName : string; + flag : string; + rtl? : boolean; +} diff --git a/src/types/i18n/index.ts b/src/types/i18n/index.ts new file mode 100644 index 0000000..e394b19 --- /dev/null +++ b/src/types/i18n/index.ts @@ -0,0 +1,5 @@ +/** + * i18n Types - Public API + */ + +export * from './i18n.types'; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..0252504 --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,22 @@ +/** + * ============================================================================ + * TYPES - Public API + * ============================================================================ + * + * Sistema de tipos completo del Visual Configuration Engine + */ + +// Core Types +export * from './core'; + +// i18n Types +export * from './i18n'; + +// Logic Types +export * from './logic'; + +// Model Types +export * from './model'; + +// View Types +export * from './view'; diff --git a/src/types/logic/index.ts b/src/types/logic/index.ts new file mode 100644 index 0000000..99e4528 --- /dev/null +++ b/src/types/logic/index.ts @@ -0,0 +1,5 @@ +/** + * Logic Types - Public API + */ + +export * from './json-logic.types'; diff --git a/src/types/logic/json-logic.types.ts b/src/types/logic/json-logic.types.ts new file mode 100644 index 0000000..911a4d6 --- /dev/null +++ b/src/types/logic/json-logic.types.ts @@ -0,0 +1,17 @@ +/** + * ============================================================================ + * JSON LOGIC TYPES + * ============================================================================ + */ + +/** + * Expresión JsonLogic + * Permite lógica condicional declarativa + */ +export type JsonLogic = + | { var: string } + | { [op: string]: any } + | string + | number + | boolean + | null; diff --git a/src/types/model/attribute.types.ts b/src/types/model/attribute.types.ts new file mode 100644 index 0000000..7e628c6 --- /dev/null +++ b/src/types/model/attribute.types.ts @@ -0,0 +1,164 @@ +/** + * ============================================================================ + * ATTRIBUTE TYPES + * ============================================================================ + */ +import type { I18nString } from '../i18n'; +import type { Metadata, Value, DataType } from '../core'; +import type { AttrID, OptionID, SectionID, JsonLogic } from '@/types'; + +// ============================================================================ +// ATTRIBUTE DISPLAY +// ============================================================================ + +/** + * Configuración de visualización de un atributo + * Los flags pueden ser booleanos o expresiones condicionales + */ +export interface AttributeDisplay { + /** + * Si se muestra en UI + * - boolean: true/false estático + * - JsonLogic: Evaluado dinámicamente + */ + uiVisible?: boolean | JsonLogic; + + /** + * Si afecta a la visualización + */ + affectsVisual?: boolean | JsonLogic; + + /** + * Si afecta al precio + */ + affectsPrice?: boolean | JsonLogic; + + /** + * Si es de solo lectura + */ + readonly?: boolean; + + /** + * Orden de visualización + */ + order?: number; + + /** + * Icono + */ + icon?: string; + + /** + * Texto de ayuda + */ + helpText?: I18nString; +} + +// ============================================================================ +// ATTRIBUTE TYPES +// ============================================================================ + +export type AttributeType = + 'fixed' | + 'dynamic' | + 'quantifiable' | + 'computed'; + + +export type AttributeCategory = + 'physical' | + 'aesthetic' | + 'functional' | + 'structural' | + 'technical' | + 'financial'; + + +// ============================================================================ +// BASE ATTRIBUTE +// ============================================================================ + +export interface BaseAttribute { + id : AttrID; + name : I18nString; + description : I18nString; + type : AttributeType; + category? : AttributeCategory; + display : AttributeDisplay; + metadata? : Metadata; +} + +// ============================================================================ +// FIXED ATTRIBUTE +// ============================================================================ + +export interface FixedAttribute extends BaseAttribute { + type : 'fixed'; + dataType: DataType; + value : Value; + unit? : I18nString; +} + +// ============================================================================ +// DYNAMIC ATTRIBUTE +// ============================================================================ + +export interface DynamicAttribute extends BaseAttribute { + type : 'dynamic'; + dataType : 'reference'; + defaultValue : OptionID; + options : AttributeOption[]; + required? : boolean; + isController? : boolean; + controls? : SectionID[]; + filterExpression?: JsonLogic; +} + +export interface AttributeOption { + optionId : OptionID; + priority? : number; + pricingOverride? : number; + metadata? : Metadata; +} + +// ============================================================================ +// QUANTIFIABLE ATTRIBUTE +// ============================================================================ + +export interface QuantifiableAttribute extends BaseAttribute { + type : 'quantifiable'; + dataType : 'number'; + quantity : QuantityDefinition; + unit? : I18nString; + userConfigurable?: boolean; + pricePerUnit? : number; + min? : number; + max? : number; +} + +export type QuantityDefinition = + | number + | { min: number; max: number; step?: number; default: number } + | { values: number[]; default: number }; + +// ============================================================================ +// COMPUTED ATTRIBUTE +// ============================================================================ + +export interface ComputedAttribute extends BaseAttribute { + type : 'computed'; + dataType : DataType; + unit? : I18nString; + expression : JsonLogic; + dependencies: AttrID[]; +} + +// ============================================================================ +// UNION TYPE +// ============================================================================ + +export type Attribute = + | FixedAttribute + | DynamicAttribute + | QuantifiableAttribute + | ComputedAttribute; diff --git a/src/types/model/catalog.types.ts b/src/types/model/catalog.types.ts new file mode 100644 index 0000000..6ab0180 --- /dev/null +++ b/src/types/model/catalog.types.ts @@ -0,0 +1,28 @@ +/** + * ============================================================================ + * CONFIGURATION CATALOG TYPES + * ============================================================================ + */ + +import type { I18nString } from '../i18n'; +import type { + OptionID, + ObjectID, + RuleID, + OptionDefinition, + ConfigurableObject, + ValidationRule +} from '@/types'; + + + +export interface ConfigurationCatalog { + name : I18nString; + description? : I18nString; + supportedLocales: string[]; + defaultLocale : string; + + options : Record; + objects : Record; + rules? : Record; +} diff --git a/src/types/model/ids.types.ts b/src/types/model/ids.types.ts new file mode 100644 index 0000000..75848be --- /dev/null +++ b/src/types/model/ids.types.ts @@ -0,0 +1,18 @@ +/** + * ============================================================================ + * MODEL - Branded ID Types + * ============================================================================ + */ + +import type { ID } from '../core'; + +/** + * ID específicos con prefijos + */ +export type AttrID = ID<'at'>; +export type SectionID = ID<'sc'>; +export type OptionID = ID<'op'>; +export type ObjectID = ID<'ob'>; +export type RuleID = ID<'rl'>; +export type ViewID = ID<'vw'>; +export type HotspotID = ID<'hs'>; diff --git a/src/types/model/index.ts b/src/types/model/index.ts new file mode 100644 index 0000000..8e8ba9e --- /dev/null +++ b/src/types/model/index.ts @@ -0,0 +1,25 @@ +/** + * Model Types - Public API + */ + +// IDs +export * from './ids.types'; + +// Attributes +export * from './attribute.types'; + +// Sections +export * from './section.types'; + +// Objects +export * from './object.types'; + +// Options +export * from './option.types'; + +// Rules +export * from './rule.types'; + +// Catalog +export * from './catalog.types'; + diff --git a/src/types/model/object.types.ts b/src/types/model/object.types.ts new file mode 100644 index 0000000..bd341b4 --- /dev/null +++ b/src/types/model/object.types.ts @@ -0,0 +1,22 @@ +/** + * ============================================================================ + * CONFIGURABLE OBJECT TYPES + * ============================================================================ + */ + +import type { I18nString } from '../i18n'; +import type { Metadata } from '../core'; +import type { ObjectID, SectionID, Attribute, Section } from '@/types'; + + + +export interface ConfigurableObject { + id : ObjectID; + name : I18nString; + description : I18nString; + attributes : Attribute[]; + sections : Record; + sectionOrder?: SectionID[]; + category? : string; + metadata? : Metadata; +} diff --git a/src/types/model/option.types.ts b/src/types/model/option.types.ts new file mode 100644 index 0000000..91ad7b6 --- /dev/null +++ b/src/types/model/option.types.ts @@ -0,0 +1,22 @@ +/** + * ============================================================================ + * OPTION DEFINITION TYPES + * ============================================================================ + */ + +import type {Pricing,Metadata,Media} from "@/types/core"; +import type { OptionID } from '@/types'; +import type { I18nString } from '../i18n'; + + + +export interface OptionDefinition { + id : OptionID; + name : I18nString; + description : I18nString; + media? : Media; + pricing? : Pricing; + tags? : string[]; + style? : string; + metadata? : Metadata; +} diff --git a/src/types/model/rule.types.ts b/src/types/model/rule.types.ts new file mode 100644 index 0000000..76abfd8 --- /dev/null +++ b/src/types/model/rule.types.ts @@ -0,0 +1,31 @@ +/** + * ============================================================================ + * VALIDATION RULE TYPES + * ============================================================================ + */ + + +import type { RuleID, AttrID ,JsonLogic } from '@/types'; +import type { Value } from '../core'; +import type { I18nString } from '../i18n'; +import type {Severity} from "@/types/core"; + + + + +export interface ValidationRule { + id : RuleID; + name : I18nString; + condition : JsonLogic; + //TODO sacar como tipos + action: { + type: 'allow' | 'forbid' | 'require' | 'suggest'; + targetAttribute: AttrID; + values: Value[]; + }; + priority : number; + bidirectional: boolean; + affects : AttrID[]; + severity : Severity; + message : I18nString; +} diff --git a/src/types/model/section.types.ts b/src/types/model/section.types.ts new file mode 100644 index 0000000..96c35e6 --- /dev/null +++ b/src/types/model/section.types.ts @@ -0,0 +1,94 @@ +/** + * ============================================================================ + * SECTION TYPES + * ============================================================================ + */ +import type { I18nString } from '../i18n'; +import type {APIConfig, Metadata} from '../core'; +import type { + AttrID, + SectionID, + Attribute, + JsonLogic } from '@/types'; + +// ============================================================================ +// RENDERING STRATEGY +// ============================================================================ + +export type RenderingStrategy = + | 'static_image' + | 'dynamic_image' + | 'api_generated' + | 'three_d' + | 'composite_layers' + | 'none'; + +// ============================================================================ +// VISUAL CONFIG +// ============================================================================ + + + +export interface SectionVisualConfig { + strategy : RenderingStrategy; + + /** Template para URL estática */ + imageUrlTemplate? : string; + + /** Atributos globales que afectan la imagen */ + globalAttributeDependencies? : AttrID[]; + + /** Atributos de sección que afectan la imagen */ + sectionAttributeDependencies?: AttrID[]; + + /** Ruta base */ + basePath? : string; + + /** Config para generación async via API */ + //TODO sacar como tipo + apiConfig? : APIConfig; + + /** Config 3D */ + threeDConfig?: { + modelUrl : string; + textureAttributes: Array<{ + attributeId: AttrID; + scope: 'global' | 'section'; + }>; + }; + + /** Imagen de fallback si falla generación */ + fallbackImage?: string; +} + +// ============================================================================ +// SECTION AVAILABILITY +// ============================================================================ + +export interface SectionAvailability { + mode : 'required' | 'optional' | 'conditional'; + condition? : JsonLogic; + dependsOn? : AttrID[]; + //TODO sacar como tipos + onDeactivate?: { + action : 'clear' | 'preserve' | 'reset'; + confirmWithUser?: boolean; + confirmMessage? : I18nString; + }; +} + +// ============================================================================ +// SECTION +// ============================================================================ + +export interface Section { + id : SectionID; + name : I18nString; + description : I18nString; + attributes : Attribute[]; + visualConfig: SectionVisualConfig; + availability: SectionAvailability; + order? : number; + icon? : string; + metadata? : Metadata; +} diff --git a/src/types/view/index.ts b/src/types/view/index.ts new file mode 100644 index 0000000..aadc36f --- /dev/null +++ b/src/types/view/index.ts @@ -0,0 +1,5 @@ +/** + * View Types - Public API + */ + +export * from './view.types'; diff --git a/src/types/view/view.types.ts b/src/types/view/view.types.ts new file mode 100644 index 0000000..2aefc8d --- /dev/null +++ b/src/types/view/view.types.ts @@ -0,0 +1,150 @@ +/** + * ============================================================================ + * VIEW TYPES - Visual Representation + * ============================================================================ + */ + +import type { I18nString } from '../i18n'; +import type { ViewID, HotspotID, AttrID , RenderingStrategy, APIConfig, JsonLogic} from "@/types"; + +// ============================================================================ +// VIEW TYPES +// ============================================================================ + +/** + * Tipos de vista predefinidos + */ +export type ViewType = + | 'front' // Vista frontal + | 'side' // Vista lateral + | 'rear' // Vista posterior + | 'top' // Vista desde arriba + | 'isometric' // Vista isométrica/3D + | 'panoramic' // Vista 360° panorámica + | 'detail' // Vista de detalle/zoom + | '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 */ + imageUrlTemplate?: string; + + /** Ruta base de imágenes */ + basePath?: string; + + /** Atributos globales que afectan a esta vista */ + globalAttributeDependencies?: AttrID[]; + + /** Atributos de sección que afectan a esta vista */ + sectionAttributeDependencies?: AttrID[]; + + /** Imagen de fallback */ + fallbackImage?: string; + + /** Config para API */ + apiConfig?: APIConfig ; + + /** Config 3D */ + threeDConfig?: { + modelUrl: string; + cameraPosition?: { x: number; y: number; z: number }; + textureAttributes?: Array<{ + attributeId: 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; +} diff --git a/src/utils/equality/deep-equal.ts b/src/utils/equality/deep-equal.ts new file mode 100644 index 0000000..1982c42 --- /dev/null +++ b/src/utils/equality/deep-equal.ts @@ -0,0 +1,34 @@ +/** + * ============================================================================ + * DEEP EQUALITY UTILITY + * ============================================================================ + */ + +import type { Value } from '../../types/core'; + +/** + * Deep equality check para valores + */ +export function deepEqual(a: Value, b: Value): boolean { + // 1. Chequeo de identidad simple (cubre primitivos iguales) + if (a === b) return true; + + // 2. Si alguno de los dos NO es un objeto o es null, devuelve false + if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) { + return false; + } + + // 3. Dates + if (a instanceof Date && b instanceof Date) { + return a.getTime() === b.getTime(); + } + + // 4. Arrays + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) return false; + return a.every((val, idx) => deepEqual(val, b[idx])); + } + + // 5. Otros tipos + return false; +} diff --git a/src/utils/equality/index.ts b/src/utils/equality/index.ts new file mode 100644 index 0000000..3edf05a --- /dev/null +++ b/src/utils/equality/index.ts @@ -0,0 +1,5 @@ +/** + * Equality Utilities - Public API + */ + +export * from './deep-equal'; diff --git a/src/utils/i18n/builder.utils.ts b/src/utils/i18n/builder.utils.ts new file mode 100644 index 0000000..4f6763e --- /dev/null +++ b/src/utils/i18n/builder.utils.ts @@ -0,0 +1,67 @@ +/** + * ============================================================================ + * I18N BUILDER UTILITIES + * ============================================================================ + */ + +import type { I18nString, I18nTranslations, SupportedLocale } from '@/types'; +import { DEFAULT_LOCALE, SUPPORTED_LOCALES } from '@/constants'; +import { translate } from '@/utils'; + +/** + * Crear I18nString desde objeto parcial + * Garantiza que al menos existe el locale por defecto + */ +export function createI18nString( + translations: Partial> +): I18nString { + if (!translations[DEFAULT_LOCALE]) { + throw new Error(`I18nString must have at least '${DEFAULT_LOCALE}' translation`); + } + return translations as I18nTranslations; +} + +/** + * Crear I18nString simple (mismo texto en todos los idiomas) + */ +export function createSimpleI18nString(text: string): I18nString { + return text; +} + +/** + * Convertir string simple a objeto de traducciones + * (Útil para migración) + */ +export function expandToTranslations( + text: I18nString, + defaultLocale: SupportedLocale = DEFAULT_LOCALE +): I18nTranslations { + if (typeof text === 'string') { + return { [defaultLocale]: text } as I18nTranslations; + } + return text; +} + +/** + * Combinar múltiples I18nStrings + * (Útil para concatenar textos) + */ +export function combineI18nStrings( + strings: I18nString[], + separator: string = ' ' +): I18nString { + // Si todos son strings simples, concatenar directamente + if (strings.every(s => typeof s === 'string')) { + return strings.join(separator); + } + + // Crear objeto con todas las traducciones + const combined: Partial> = {}; + + for (const locale of SUPPORTED_LOCALES) { + const parts = strings.map(s => translate(s, locale)); + combined[locale] = parts.join(separator); + } + + return combined as I18nTranslations; +} diff --git a/src/utils/i18n/index.ts b/src/utils/i18n/index.ts new file mode 100644 index 0000000..5dbf25b --- /dev/null +++ b/src/utils/i18n/index.ts @@ -0,0 +1,8 @@ +/** + * i18n Utilities - Public API + */ + +export * from './translation.utils'; +export * from './locale.utils'; +export * from './builder.utils'; +export * from './validation.utils'; diff --git a/src/utils/i18n/locale.utils.ts b/src/utils/i18n/locale.utils.ts new file mode 100644 index 0000000..739cf97 --- /dev/null +++ b/src/utils/i18n/locale.utils.ts @@ -0,0 +1,40 @@ +/** + * ============================================================================ + * LOCALE UTILITIES + * ============================================================================ + */ + +import type { SupportedLocale, LocaleInfo } from '@/types'; +import { SUPPORTED_LOCALES, LOCALE_INFO, DEFAULT_LOCALE } from '@/constants'; + +/** + * Verificar si un string es un locale soportado + */ +export function isSupportedLocale(locale: string): locale is SupportedLocale { + return SUPPORTED_LOCALES.includes(locale as SupportedLocale); +} + +/** + * Obtener locale válido con fallback + */ +export function getValidLocale( + locale: string | undefined, + fallback: SupportedLocale = DEFAULT_LOCALE +): SupportedLocale { + if (!locale) return fallback; + return isSupportedLocale(locale) ? locale : fallback; +} + +/** + * Extraer código de idioma de un locale (ej: "es-ES" → "es") + */ +export function extractLanguageCode(locale: string): string { + return locale.split('-')[0].toLowerCase(); +} + +/** + * Obtener información de un locale + */ +export function getLocaleInfo(locale: SupportedLocale): LocaleInfo { + return LOCALE_INFO[locale]; +} diff --git a/src/utils/i18n/translation.utils.ts b/src/utils/i18n/translation.utils.ts new file mode 100644 index 0000000..c773713 --- /dev/null +++ b/src/utils/i18n/translation.utils.ts @@ -0,0 +1,93 @@ +/** + * ============================================================================ + * TRANSLATION UTILITIES + * ============================================================================ + */ + +import type { I18nString, SupportedLocale } from '../../types/i18n'; +import { DEFAULT_LOCALE } from '../../constants'; + +/** + * Obtener texto traducido + * + * @param text - String i18n (simple o con traducciones) + * @param locale - Locale objetivo + * @param fallback - Locale de respaldo si no existe la traducción + * @returns Texto en el idioma solicitado + * + * @example + * const text: I18nString = { es: "Hola", en: "Hello" }; + * translate(text, 'es') // → "Hola" + * translate(text, 'fr') // → "Hola" (fallback a español) + * translate("OK", 'es') // → "OK" (string simple) + */ +export function translate( + text: I18nString, + locale: SupportedLocale, + fallback: SupportedLocale = DEFAULT_LOCALE +): string { + // Si es string simple, devolverlo directamente + if (typeof text === 'string') { + return text; + } + + // Intentar con el locale solicitado + if (text[locale]) { + return text[locale]; + } + + // Fallback al locale de respaldo + if (text[fallback]) { + return text[fallback]; + } + + // Último recurso: primer valor disponible + const firstAvailable = Object.values(text)[0]; + return firstAvailable || ''; +} + +/** + * Alias corto de translate + */ +export const t = translate; + +/** + * Verificar si un I18nString tiene traducción para un locale + */ +export function hasTranslation( + text: I18nString, + locale: SupportedLocale +): boolean { + if (typeof text === 'string') return true; + return text[locale] !== undefined; +} + +/** + * Obtener todos los locales disponibles en un I18nString + */ +export function getAvailableLocales(text: I18nString): SupportedLocale[] { + if (typeof text === 'string') { + // String simple está disponible en todos los locales + return [...SUPPORTED_LOCALES]; + } + return Object.keys(text) as SupportedLocale[]; +} + +/** + * Verificar si un I18nString está completo (tiene todas las traducciones) + */ +export function isComplete(text: I18nString): boolean { + if (typeof text === 'string') return true; + return SUPPORTED_LOCALES.every(locale => text[locale] !== undefined); +} + +/** + * Obtener locales faltantes en un I18nString + */ +export function getMissingLocales(text: I18nString): SupportedLocale[] { + if (typeof text === 'string') return []; + return SUPPORTED_LOCALES.filter(locale => text[locale] === undefined); +} + +// Re-export SUPPORTED_LOCALES for convenience +import { SUPPORTED_LOCALES } from '@/constants'; diff --git a/src/utils/i18n/validation.utils.ts b/src/utils/i18n/validation.utils.ts new file mode 100644 index 0000000..87f4182 --- /dev/null +++ b/src/utils/i18n/validation.utils.ts @@ -0,0 +1,46 @@ +/** + * ============================================================================ + * I18N VALIDATION UTILITIES + * ============================================================================ + */ + +import type { I18nString } from '@/types'; +import { DEFAULT_LOCALE } from '@/constants'; + +/** + * Validar que un I18nString es correcto + */ +export function validateI18nString(text: unknown): text is I18nString { + if (typeof text === 'string') return true; + + if (typeof text !== 'object' || text === null) return false; + + const obj = text as Record; + + // Debe tener al menos el locale por defecto + if (typeof obj[DEFAULT_LOCALE] !== 'string') return false; + + // Todos los valores deben ser strings + return Object.values(obj).every(v => typeof v === 'string'); +} + +/** + * Error para I18nString inválido + */ +export class I18nStringError extends Error { + constructor(message: string) { + super(`[I18n] ${message}`); + this.name = 'I18nStringError'; + } +} + +/** + * Validar y lanzar error si es inválido + */ +export function assertValidI18nString(text: unknown): asserts text is I18nString { + if (!validateI18nString(text)) { + throw new I18nStringError( + `Invalid I18nString. Must be a string or object with at least '${DEFAULT_LOCALE}' key.` + ); + } +} diff --git a/src/utils/ids/id-parser.ts b/src/utils/ids/id-parser.ts new file mode 100644 index 0000000..8aecda0 --- /dev/null +++ b/src/utils/ids/id-parser.ts @@ -0,0 +1,362 @@ +// utils/ids-improved.ts +/** + * ============================================================================ + * ID UTILITIES - VERSIÓN MEJORADA + * ============================================================================ + * + * Sistema robusto y type-safe para manejar IDs con prefijos + * + * Mejoras sobre versión original: + * - Cache para performance + * - Validación centralizada + * - Error handling robusto + * - Type-safe parsing + * - Fácil de testear + * + * ============================================================================ + */ + +import type {AttrID, ObjectID, OptionID, RuleID, SectionID, HotspotID, ViewID, ID} from "@/types"; +import {ERRORS, logger, MESSAGE_CATEGORIES} from "@/messages"; + +// ============================================================================ +// TYPES +// ============================================================================ + +export type IDPrefix = 'at' | 'op' | 'sc' | 'ob' | 'rl' | 'vw' | 'hs'; + +export interface ParsedID { + prefix: string; + value: string; + fullId: string; +} + +// ============================================================================ +// CACHE +// ============================================================================ + +/** + * Cache LRU simple para parsing de IDs + * Evita splits repetitivos en hot paths + */ +class IDCache { + private cache = new Map(); + private maxSize = 500; + + get(id: string): string | undefined { + return this.cache.get(id); + } + + set(id: string, value: string): void { + if (this.cache.size >= this.maxSize) { + const firstKey = this.cache.keys().next().value; + if (firstKey !== undefined) { + this.cache.delete(firstKey); + } + } + this.cache.set(id, value); + } + + clear(): void { + this.cache.clear(); + } +} + +const idPartCache = new IDCache(); + +// ============================================================================ +// VALIDATION +// ============================================================================ + +/** + * Valida que un string tenga formato de ID válido + */ +export function isValidId(id: string): boolean { + if (!id) return false; + + const parts = id.split(':'); + if (parts.length !== 2) return false; + + const [prefix, value] = parts; + return !!prefix && !!value; +} + +/** + * Valida que un ID tenga el prefijo esperado + */ +export function hasPrefix(id: string, expectedPrefix: T): boolean { + const parts = id.split(':'); + return parts[0] === expectedPrefix; +} + +// ============================================================================ +// PARSING +// ============================================================================ + +/** + * Parsea un ID en sus componentes + * + * @throws {Error} Si el ID tiene formato inválido + * + * @example + * parseId("at:suelo") // → { prefix: "at", value: "suelo", fullId: "at:suelo" } + */ +export function parseId(id: string): ParsedID { + if (!isValidId(id)) { + const errorMsg = ERRORS.INVALID_ID_FORMAT(id); + logger.error(MESSAGE_CATEGORIES.ID, errorMsg, { id }); + throw new Error(JSON.stringify(errorMsg)); + } + + const [prefix, value] = id.split(':'); + + return { + prefix, + value, + fullId: id + }; +} + +/** + * Extrae solo la parte del valor del ID (después del prefijo) + * Con cache para performance + * + * @param id - ID completo (ej: "at:suelo") + * @param expectedPrefix - Prefijo esperado (opcional, para validación) + * @returns La parte del valor (ej: "suelo") + * + * @throws {Error} Si el ID es inválido o no coincide el prefijo + * + * @example + * extractIdPart("at:suelo") // → "suelo" + * extractIdPart("at:suelo", "at") // → "suelo" ✅ + * extractIdPart("at:suelo", "sc") // → throws Error ❌ + */ +export function extractIdPart( + id: ID, + expectedPrefix?: T +): string { + // Check cache first + const cacheKey = expectedPrefix ? `${id}:${expectedPrefix}` : id; + const cached = idPartCache.get(cacheKey); + if (cached !== undefined) { + return cached; + } + + // Parse and validate + const parsed = parseId(id); + + // Validate prefix if provided + if (expectedPrefix && parsed.prefix !== expectedPrefix) { + throw new Error( + `ID prefix mismatch: expected "${expectedPrefix}:", got "${parsed.prefix}:" in ID "${id}"` + ); + } + + // Cache and return + idPartCache.set(cacheKey, parsed.value); + return parsed.value; +} + +/** + * Versión safe de extractIdPart que retorna undefined en lugar de throw + */ +export function tryExtractIdPart( + id: ID, + expectedPrefix?: T +): string | undefined { + try { + return extractIdPart(id, expectedPrefix); + } catch { + return undefined; + } +} + +// ============================================================================ +// BUILDERS +// ============================================================================ + +/** + * Crea IDs type-safe con validación + * Mejora sobre versión original que no valida + */ +export const IDS = { + /** + * Crea un AttrID + * @throws {Error} Si el valor es inválido + */ + attr: (id: string): AttrID => { + if (!id || id.includes(':')) { + throw new Error(`Invalid attr ID value: "${id}". Must not be empty or contain ":"`); + } + return `at:${id}`; + }, + + opt: (id: string): OptionID => { + if (!id || id.includes(':')) { + throw new Error(`Invalid option ID value: "${id}"`); + } + return `op:${id}`; + }, + + sec: (id: string): SectionID => { + if (!id || id.includes(':')) { + throw new Error(`Invalid section ID value: "${id}"`); + } + return `sc:${id}`; + }, + + obj: (id: string): ObjectID => { + if (!id || id.includes(':')) { + throw new Error(`Invalid object ID value: "${id}"`); + } + return `ob:${id}`; + }, + + rule: (id: string): RuleID => { + if (!id || id.includes(':')) { + throw new Error(`Invalid rule ID value: "${id}"`); + } + return `rl:${id}`; + }, + + view: (id: string): ViewID => { + if (!id || id.includes(':')) { + throw new Error(`Invalid view ID value: "${id}"`); + } + return `vw:${id}`; + }, + + hspot: (id: string): HotspotID => { + if (!id || id.includes(':')) { + throw new Error(`Invalid hotspot ID value: "${id}"`); + } + return `hs:${id}`; + } +}; + +/** + * Versión safe de IDS que retorna undefined en lugar de throw + */ +export const SafeIDS = { + attr: (id: string): AttrID | undefined => { + try { return IDS.attr(id); } catch { return undefined; } + }, + opt: (id: string): OptionID | undefined => { + try { return IDS.opt(id); } catch { return undefined; } + }, + sec: (id: string): SectionID | undefined => { + try { return IDS.sec(id); } catch { return undefined; } + }, + obj: (id: string): ObjectID | undefined => { + try { return IDS.obj(id); } catch { return undefined; } + }, + rule: (id: string): RuleID | undefined => { + try { return IDS.rule(id); } catch { return undefined; } + }, + view: (id: string): ViewID | undefined => { + try { return IDS.view(id); } catch { return undefined; } + }, + hspot: (id: string): HotspotID | undefined => { + try { return IDS.hspot(id); } catch { return undefined; } + } +}; + +// ============================================================================ +// COLLECTIONS +// ============================================================================ + +/** + * Extrae todas las partes de valor de una colección de IDs + * + * @example + * extractAllIdParts(["at:suelo", "at:pared"]) + * // → ["suelo", "pared"] + */ +export function extractAllIdParts( + ids: ID[], + expectedPrefix?: T +): string[] { + return ids.map(id => extractIdPart(id, expectedPrefix)); +} + +/** + * Convierte un array de IDs a un objeto Record con las partes como keys + * + * @example + * idsToRecord(["at:suelo", "at:pared"], (id) => attributes[id]) + * // → { suelo: Value1, pared: Value2 } + */ +export function idsToRecord( + ids: ID[], + getValue: (id: ID) => V, + expectedPrefix?: T +): Record { + const record: Record = {}; + + for (const id of ids) { + const part = extractIdPart(id, expectedPrefix); + record[part] = getValue(id); + } + + return record; +} + +// ============================================================================ +// UTILITIES +// ============================================================================ + +/** + * Limpia el cache de IDs + * Útil para tests o cuando se sospecha corrupción + */ +export function clearIdCache(): void { + idPartCache.clear(); +} + +/** + * Normaliza un ID (asegura formato consistente) + * Útil para IDs que vienen de input de usuario + */ +export function normalizeId(id: string, expectedPrefix?: IDPrefix): string { + const parsed = parseId(id); + + if (expectedPrefix && parsed.prefix !== expectedPrefix) { + throw new Error(`Expected prefix "${expectedPrefix}", got "${parsed.prefix}"`); + } + + // Normalizar el value: lowercase, trim, remove espacios extra + const normalizedValue = parsed.value + .toLowerCase() + .trim() + .replace(/\s+/g, '_'); + + return `${parsed.prefix}:${normalizedValue}`; +} + +// ============================================================================ +// EXPORTS +// ============================================================================ + +export default { + // Validation + isValidId, + hasPrefix, + + // Parsing + parseId, + extractIdPart, + tryExtractIdPart, + + // Builders + IDS, + SafeIDS, + + // Collections + extractAllIdParts, + idsToRecord, + + // Utilities + clearIdCache, + normalizeId +}; diff --git a/src/utils/ids/index.ts b/src/utils/ids/index.ts new file mode 100644 index 0000000..6e4b69f --- /dev/null +++ b/src/utils/ids/index.ts @@ -0,0 +1,5 @@ +/** + * ID Utilities - Public API + */ + +export * from './id-parser'; diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..b53aa84 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,23 @@ +/** + * ============================================================================ + * UTILITIES - Public API + * ============================================================================ + */ + +// i18n Utilities +export * from './i18n'; + +// ID Utilities +export * from './ids'; + +// URL Utilities +export * from './url'; + +// View Utilities +export * from './view'; + +// Validation Utilities +export * from './validation'; + +// Equality Utilities +export * from './equality'; diff --git a/src/utils/url/index.ts b/src/utils/url/index.ts new file mode 100644 index 0000000..7cca22e --- /dev/null +++ b/src/utils/url/index.ts @@ -0,0 +1,5 @@ +/** + * URL Utilities - Public API + */ + +export * from './url-builder'; diff --git a/src/utils/url/url-builder.ts b/src/utils/url/url-builder.ts new file mode 100644 index 0000000..60f7c30 --- /dev/null +++ b/src/utils/url/url-builder.ts @@ -0,0 +1,387 @@ +// utils/url-builder.ts +/** + * ============================================================================ + * URL TEMPLATE BUILDER - VERSIÓN MEJORADA + * ============================================================================ + * + * Centraliza toda la lógica de generación de URLs con templates + * Elimina duplicación entre vce.ts y view.ts + * + * ============================================================================ + */ + +import type { AttrID, SectionID, ViewID } from "../../types/model"; +import type { Value } from "../../types/core"; +import { extractIdPart } from "../ids"; + +// ============================================================================ +// TYPES +// ============================================================================ + +export interface UrlTemplateContext { + /** Ruta base opcional */ + basePath?: string; + + /** ID de sección */ + sectionId?: SectionID; + + /** ID de vista */ + viewId?: ViewID; + + /** Atributos globales disponibles */ + globalAttributes?: Record; + + /** Atributos de sección disponibles */ + sectionAttributes?: Record; + + /** IDs de atributos globales que el template necesita */ + globalDependencies?: AttrID[]; + + /** IDs de atributos de sección que el template necesita */ + sectionDependencies?: AttrID[]; +} + +export interface UrlBuildResult { + /** URL generada (o fallback si hubo errores) */ + url: string; + + /** Placeholders que no se pudieron resolver */ + unresolvedPlaceholders: string[]; + + /** Advertencias durante la generación */ + warnings: string[]; + + /** Si se usó la URL de fallback */ + usedFallback: boolean; +} + +export interface UrlBuilderOptions { + /** URL de fallback si falla la generación */ + fallbackUrl?: string; + + /** Modo de logging: 'none' | 'console' | 'collect' */ + logMode?: 'none' | 'console' | 'collect'; + + /** Estrategia cuando falta un atributo: 'default' | 'skip' | 'error' */ + onMissingAttribute?: 'default' | 'skip' | 'error'; +} + +// ============================================================================ +// URL TEMPLATE BUILDER +// ============================================================================ + +export class UrlTemplateBuilder { + private warnings: string[] = []; + private readonly fallbackUrl: string; + private readonly options: Required; + + constructor( + private template: string, + options: UrlBuilderOptions = {} + ) { + this.fallbackUrl = options.fallbackUrl || '/images/fallback.jpg'; + this.options = { + fallbackUrl: this.fallbackUrl, + logMode: options.logMode || 'console', + onMissingAttribute: options.onMissingAttribute || 'default' + }; + } + + /** + * Construye la URL reemplazando todos los placeholders + */ + build(context: UrlTemplateContext): UrlBuildResult { + this.warnings = []; + let url = this.template; + + try { + // 1. Reemplazar basePath + if (context.basePath) { + url = this.replaceSimpleToken(url, 'basePath', context.basePath); + } + + // 2. Reemplazar sectionId + if (context.sectionId) { + const sectionIdPart = extractIdPart(context.sectionId, 'sc'); + url = this.replaceSimpleToken(url, 'sectionId', sectionIdPart); + } + + // 3. Reemplazar viewId + if (context.viewId) { + const viewIdPart = extractIdPart(context.viewId, 'vw'); + url = this.replaceSimpleToken(url, 'viewId', viewIdPart); + } + + // 4. Reemplazar atributos globales + if (context.globalDependencies && context.globalAttributes) { + url = this.replaceAttributes( + url, + 'global', + context.globalDependencies, + context.globalAttributes + ); + } + + // 5. Reemplazar atributos de sección + if (context.sectionDependencies && context.sectionAttributes) { + url = this.replaceAttributes( + url, + 'section', + context.sectionDependencies, + context.sectionAttributes + ); + } + + // 6. Verificar placeholders sin resolver + const unresolved = this.findUnresolvedPlaceholders(url); + + if (unresolved.length > 0) { + this.addWarning( + `Unresolved placeholders: ${unresolved.join(', ')}`, + { template: this.template, partialUrl: url } + ); + + return { + url: this.fallbackUrl, + unresolvedPlaceholders: unresolved, + warnings: this.warnings, + usedFallback: true + }; + } + + return { + url, + unresolvedPlaceholders: [], + warnings: this.warnings, + usedFallback: false + }; + + } catch (error) { + this.addWarning( + `Error building URL: ${error instanceof Error ? error.message : String(error)}`, + { template: this.template, context } + ); + + return { + url: this.fallbackUrl, + unresolvedPlaceholders: [], + warnings: this.warnings, + usedFallback: true + }; + } + } + + /** + * Reemplaza un token simple {key} → value + */ + private replaceSimpleToken(url: string, key: string, value: string): string { + const placeholder = `{${key}}`; + return url.replace(placeholder, value); + } + + /** + * Reemplaza placeholders de atributos: {scope.attrName} + */ + private replaceAttributes( + url: string, + scope: 'global' | 'section', + dependencies: AttrID[], + attributes: Record + ): string { + for (const attrId of dependencies) { + const attrName = extractIdPart(attrId, 'at'); + const placeholder = `{${scope}.${attrName}}`; + + // Skip si el placeholder no existe en el template + if (!url.includes(placeholder)) { + continue; + } + + const value = attributes[attrId]; + + if (value !== undefined) { + const normalized = this.normalizeValueForUrl(value); + url = url.replace(placeholder, normalized); + } else { + // Manejar atributo faltante según estrategia + url = this.handleMissingAttribute( + url, + placeholder, + scope, + attrId + ); + } + } + + return url; + } + + /** + * Normaliza un valor para usar en URL + * - Lowercase + * - Espacios → underscores + * - Solo caracteres alfanuméricos y guiones + */ + private normalizeValueForUrl(value: Value): string { + // Arrays: join with dash + if (Array.isArray(value)) { + return value.map(v => this.normalizeValueForUrl(v)).join('-'); + } + + // Dates: ISO format + if (value instanceof Date) { + return value.toISOString().split('T')[0]; // YYYY-MM-DD + } + + // Todo lo demás: string + return String(value) + .toLowerCase() + .trim() + .replace(/\s+/g, '_') + .replace(/[^a-z0-9_-]/g, ''); + } + + /** + * Maneja el caso de atributo faltante según configuración + */ + private handleMissingAttribute( + url: string, + placeholder: string, + scope: string, + attrId: AttrID + ): string { + const strategy = this.options.onMissingAttribute; + + switch (strategy) { + case 'default': + this.addWarning( + `Missing ${scope} attribute: ${attrId}, using 'default'`, + { placeholder } + ); + return url.replace(placeholder, 'default'); + + case 'skip': + this.addWarning( + `Missing ${scope} attribute: ${attrId}, skipping replacement`, + { placeholder } + ); + return url; // No reemplazar + + case 'error': + throw new Error( + `Missing required ${scope} attribute: ${attrId} for placeholder ${placeholder}` + ); + } + } + + /** + * Encuentra placeholders que no fueron reemplazados + */ + private findUnresolvedPlaceholders(url: string): string[] { + const matches = url.match(/\{[^}]+}/g); + return matches || []; + } + + /** + * Añade una advertencia al log + */ + private addWarning(message: string, context?: Record): void { + this.warnings.push(message); + + if (this.options.logMode === 'console') { + console.warn('[UrlBuilder]', message, context); + } + } +} + +// ============================================================================ +// HELPER FUNCTIONS +// ============================================================================ + +/** + * Genera URL para una sección + * Wrapper conveniente sobre UrlTemplateBuilder + */ +export function buildSectionImageUrl( + template: string, + sectionId: SectionID, + config: { + basePath?: string; + fallbackImage?: string; + globalAttributeDependencies?: AttrID[]; + sectionAttributeDependencies?: AttrID[]; + }, + globalAttributes: Record, + sectionAttributes: Record +): string { + if (!template) { + console.warn(`[UrlBuilder] No template provided for section ${sectionId}`); + return config.fallbackImage || '/images/fallback.jpg'; + } + + const builder = new UrlTemplateBuilder(template, { + fallbackUrl: config.fallbackImage, + logMode: 'console', + onMissingAttribute: 'default' + }); + + const result = builder.build({ + basePath: config.basePath, + sectionId, + globalAttributes, + sectionAttributes, + globalDependencies: config.globalAttributeDependencies, + sectionDependencies: config.sectionAttributeDependencies + }); + + return result.url; +} + +/** + * Genera URL para una vista específica + */ +export function buildViewImageUrl( + template: string, + sectionId: SectionID, + viewId: ViewID, + config: { + basePath?: string; + fallbackImage?: string; + globalAttributeDependencies?: AttrID[]; + sectionAttributeDependencies?: AttrID[]; + }, + globalAttributes: Record, + sectionAttributes: Record +): string { + if (!template) { + return config.fallbackImage || '/images/fallback.jpg'; + } + + const builder = new UrlTemplateBuilder(template, { + fallbackUrl: config.fallbackImage, + logMode: 'console', + onMissingAttribute: 'default' + }); + + const result = builder.build({ + basePath: config.basePath, + sectionId, + viewId, + globalAttributes, + sectionAttributes, + globalDependencies: config.globalAttributeDependencies, + sectionDependencies: config.sectionAttributeDependencies + }); + + return result.url; +} + +// ============================================================================ +// EXPORTS +// ============================================================================ + +export default { + UrlTemplateBuilder, + buildSectionImageUrl, + buildViewImageUrl +}; diff --git a/src/utils/validation/attribute.validator.ts b/src/utils/validation/attribute.validator.ts new file mode 100644 index 0000000..10a1b8c --- /dev/null +++ b/src/utils/validation/attribute.validator.ts @@ -0,0 +1,57 @@ +/** + * ============================================================================ + * ATTRIBUTE VALIDATOR + * ============================================================================ + */ +import type {Attribute, AttrID, SectionID} from '@/types'; + + +export type ValidationSeverity = + 'error' | + 'warning' ; + + +export interface ValidationError { + attrId? : AttrID; + sectionId? : SectionID; + message : string; + severity : ValidationSeverity; +} + +/** + * Validar atributo en runtime + */ +export function validateAttribute(attr: Attribute): ValidationError[] { + const errors: ValidationError[] = []; + + // Computed debe ser readonly + if (attr.type === 'computed' && !attr.display.readonly) { + errors.push({ + attrId: attr.id, + message: 'Computed attributes should be readonly', + severity: 'error' + }); + } + + // Fixed no debería afectar precio + if (attr.type === 'fixed' && attr.display.affectsPrice) { + errors.push({ + attrId: attr.id, + message: 'Fixed attributes should not affect price', + severity: 'warning' + }); + } + + // Verificar que dependencies existen + if (attr.type === 'computed') { + if (!attr.dependencies || attr.dependencies.length === 0) { + errors.push({ + attrId: attr.id, + message: 'Computed attribute has no dependencies', + severity: 'warning' + }); + } + } + + return errors; +} diff --git a/src/utils/validation/index.ts b/src/utils/validation/index.ts new file mode 100644 index 0000000..5522b53 --- /dev/null +++ b/src/utils/validation/index.ts @@ -0,0 +1,6 @@ +/** + * Validation Utilities - Public API + */ + +export * from './attribute.validator'; +export * from './section.validator'; diff --git a/src/utils/validation/section.validator.ts b/src/utils/validation/section.validator.ts new file mode 100644 index 0000000..97e624f --- /dev/null +++ b/src/utils/validation/section.validator.ts @@ -0,0 +1,40 @@ +/** + * ============================================================================ + * SECTION VALIDATOR + * ============================================================================ + */ + +import type { Section, AttrID } from '@/types'; +import type { ValidationError } from '@/utils'; + +/** + * Validar sección en runtime + */ +export function validateSection(section: Section): ValidationError[] { + const errors: ValidationError[] = []; + + // Validar que imageUrlTemplate tiene placeholders válidos + if (section.visualConfig.imageUrlTemplate) { + const template = section.visualConfig.imageUrlTemplate; + const placeholders = template.match(/\{[^}]+}/g) || []; + + placeholders.forEach(placeholder => { + const cleanName = placeholder.slice(1, -1); // Remove { } + + if (cleanName.startsWith('global.')) { + const attrName = cleanName.replace('global.', ''); + const attrId = `at:${attrName}` as AttrID; + + if (!section.visualConfig.globalAttributeDependencies?.includes(attrId)) { + errors.push({ + sectionId: section.id, + message: `Placeholder ${placeholder} not in globalAttributeDependencies`, + severity: 'error' + }); + } + } + }); + } + + return errors; +} diff --git a/src/utils/view/hotspot-filter.utils.ts b/src/utils/view/hotspot-filter.utils.ts new file mode 100644 index 0000000..11574ad --- /dev/null +++ b/src/utils/view/hotspot-filter.utils.ts @@ -0,0 +1,30 @@ +/** + * ============================================================================ + * HOTSPOT FILTER UTILITIES + * ============================================================================ + */ + +import type { Hotspot, SectionView , AttrID } from '@/types'; + +/** + * Obtener hotspots visibles de una vista + */ +export function getVisibleHotspots( + view : SectionView, + attributeValues : Record, + evaluator : any +): Hotspot[] { + if (!view.hotspots) return []; + + return view.hotspots.filter(hotspot => { + if (hotspot.visible === undefined || hotspot.visible === true) { + return true; + } + if (hotspot.visible === false) { + return false; + } + // Es JsonLogic, evaluar + const data = { attributes: attributeValues }; + return evaluator.evaluate(hotspot.visible, data); + }); +} diff --git a/src/utils/view/index.ts b/src/utils/view/index.ts new file mode 100644 index 0000000..c82fa88 --- /dev/null +++ b/src/utils/view/index.ts @@ -0,0 +1,7 @@ +/** + * View Utilities - Public API + */ + +export * from './view-selector.utils'; +export * from './hotspot-filter.utils'; +export * from './view-image.utils'; diff --git a/src/utils/view/view-image.utils.ts b/src/utils/view/view-image.utils.ts new file mode 100644 index 0000000..34ae451 --- /dev/null +++ b/src/utils/view/view-image.utils.ts @@ -0,0 +1,61 @@ +/** + * ============================================================================ + * VIEW IMAGE UTILITIES + * ============================================================================ + */ + +import type { SectionView } from '../../types/view'; +import type { SectionID, AttrID } from '../../types/model'; +import type { Value } from '../../types/core'; +import { extractIdPart } from '../ids'; + +/** + * Generar URL de imagen para una vista específica + */ +export function generateViewImageUrl( + view : SectionView, + sectionId : SectionID, + globalAttributes : Record, + sectionAttributes: Record +): string { + const config = view.visualConfig; + + if (!config.imageUrlTemplate) { + return config.fallbackImage || '/images/fallback.jpg'; + } + + let url = config.imageUrlTemplate; + + // Reemplazar {basePath} + if (config.basePath) { + url = url.replace('{basePath}', config.basePath); + } + + // Reemplazar {sectionId} + const sectionIdPart = extractIdPart(sectionId, 'sc'); + url = url.replace('{sectionId}', sectionIdPart); + + // Reemplazar {viewId} + const viewIdPart = extractIdPart(view.id, 'vw'); + url = url.replace('{viewId}', viewIdPart); + + // Reemplazar atributos globales + config.globalAttributeDependencies?.forEach(attrId => { + const attrName = extractIdPart(attrId, 'at'); + const value = globalAttributes[attrId]; + if (value !== undefined) { + url = url.replace(`{global.${attrName}}`, String(value)); + } + }); + + // Reemplazar atributos de sección + config.sectionAttributeDependencies?.forEach(attrId => { + const attrName = extractIdPart(attrId, 'at'); + const value = sectionAttributes[attrId]; + if (value !== undefined) { + url = url.replace(`{section.${attrName}}`, String(value)); + } + }); + + return url; +} diff --git a/src/utils/view/view-selector.utils.ts b/src/utils/view/view-selector.utils.ts new file mode 100644 index 0000000..86d9b2f --- /dev/null +++ b/src/utils/view/view-selector.utils.ts @@ -0,0 +1,37 @@ +/** + * ============================================================================ + * VIEW SELECTOR UTILITIES + * ============================================================================ + */ + + +import type { SectionView, AttrID } from '@/types'; + +/** + * Obtener la vista por defecto de una lista de vistas + */ +export function getDefaultView(views: SectionView[]): SectionView | null { + const defaultView = views.find(v => v.isDefault === true); + return defaultView || views[0] || null; +} + +/** + * Filtrar vistas visibles según condiciones + */ +export function getVisibleViews( + views: SectionView[], + attributeValues: Record, + evaluator: any +): SectionView[] { + return views.filter(view => { + if (view.visible === undefined || view.visible === true) { + return true; + } + if (view.visible === false) { + return false; + } + // Es JsonLogic, evaluar + const data = { attributes: attributeValues }; + return evaluator.evaluate(view.visible, data); + }); +} diff --git a/srcsa/code/i18n.ts b/srcsa/code/i18n.ts new file mode 100644 index 0000000..b6d8b20 --- /dev/null +++ b/srcsa/code/i18n.ts @@ -0,0 +1,353 @@ +/** + * ============================================================================ + * INTERNATIONALIZATION (i18n) - Utilities + * ============================================================================ + * + * Sistema de internacionalización para textos multiidioma + * + * ============================================================================ + */ +import type {SupportedLocale, I18nString, I18nTranslations, LocaleInfo} from "../types/i18n.ts"; +import { DEFAULT_LOCALE } from "../types/i18n.ts"; + +// ============================================================================ +// SUPPORTED LOCALES +// ============================================================================ + +/** + * Lista de todos los locales soportados + */ +export const SUPPORTED_LOCALES: readonly SupportedLocale[] = [ + 'es', + 'en', + 'de', + 'fr', + 'it', + 'pt', + 'ca', + 'eu', + 'gl' +] as const; + + +// ============================================================================ +// LOCALE UTILITIES +// ============================================================================ + +/** + * Verificar si un string es un locale soportado + */ +export function isSupportedLocale(locale: string): locale is SupportedLocale { + return SUPPORTED_LOCALES.includes(locale as SupportedLocale); +} + +/** + * Obtener locale válido con fallback + */ +export function getValidLocale( + locale: string | undefined, + fallback: SupportedLocale = DEFAULT_LOCALE +): SupportedLocale { + if (!locale) return fallback; + return isSupportedLocale(locale) ? locale : fallback; +} + +/** + * Extraer código de idioma de un locale (ej: "es-ES" → "es") + */ +export function extractLanguageCode(locale: string): string { + return locale.split('-')[0].toLowerCase(); +} + +// ============================================================================ +// TRANSLATION UTILITIES +// ============================================================================ + +/** + * Obtener texto traducido + * + * @param text - String i18n (simple o con traducciones) + * @param locale - Locale objetivo + * @param fallback - Locale de respaldo si no existe la traducción + * @returns Texto en el idioma solicitado + * + * @example + * const text: I18nString = { es: "Hola", en: "Hello" }; + * t(text, 'es') // → "Hola" + * t(text, 'fr') // → "Hola" (fallback a español) + * t("OK", 'es') // → "OK" (string simple) + */ +export function t( + text: I18nString, + locale: SupportedLocale, + fallback: SupportedLocale = DEFAULT_LOCALE +): string { + // Si es string simple, devolverlo directamente + if (typeof text === 'string') { + return text; + } + + // Intentar con el locale solicitado + if (text[locale]) { + return text[locale]; + } + + // Fallback al locale de respaldo + if (text[fallback]) { + return text[fallback]; + } + + // Último recurso: primer valor disponible + const firstAvailable = Object.values(text)[0]; + return firstAvailable || ''; +} + +/** + * Alias de `t` para compatibilidad con librerías i18n + */ +export const translate = t; + +/** + * Verificar si un I18nString tiene traducción para un locale + */ +export function hasTranslation( + text: I18nString, + locale: SupportedLocale +): boolean { + if (typeof text === 'string') return true; + return text[locale] !== undefined; +} + +/** + * Obtener todos los locales disponibles en un I18nString + */ +export function getAvailableLocales(text: I18nString): SupportedLocale[] { + if (typeof text === 'string') return [...SUPPORTED_LOCALES]; + return Object.keys(text) as SupportedLocale[]; +} + +/** + * Verificar si un I18nString está completo (tiene todas las traducciones) + */ +export function isComplete(text: I18nString): boolean { + if (typeof text === 'string') return true; + return SUPPORTED_LOCALES.every(locale => text[locale] !== undefined); +} + +/** + * Obtener locales faltantes en un I18nString + */ +export function getMissingLocales(text: I18nString): SupportedLocale[] { + if (typeof text === 'string') return []; + return SUPPORTED_LOCALES.filter(locale => text[locale] === undefined); +} + +// ============================================================================ +// I18N BUILDERS +// ============================================================================ + +/** + * Crear I18nString desde objeto parcial + * Garantiza que al menos existe el locale por defecto + */ +export function createI18nString( + translations: Partial> +): I18nString { + if (!translations[DEFAULT_LOCALE]) { + throw new Error(`I18nString must have at least '${DEFAULT_LOCALE}' translation`); + } + return translations as I18nTranslations; +} + +/** + * Crear I18nString simple (mismo texto en todos los idiomas) + */ +export function createSimpleI18nString(text: string): I18nString { + return text; +} + +/** + * Convertir string simple a objeto de traducciones + * (útil para migración) + */ +export function expandToTranslations( + text: I18nString, + defaultLocale: SupportedLocale = DEFAULT_LOCALE +): I18nTranslations { + if (typeof text === 'string') { + return { [defaultLocale]: text } as I18nTranslations; + } + return text; +} + +/** + * Combinar múltiples I18nStrings + * (útil para concatenar textos) + */ +export function combineI18nStrings( + strings: I18nString[], + separator: string = ' ' +): I18nString { + // Si todos son strings simples, concatenar directamente + if (strings.every(s => typeof s === 'string')) { + return strings.join(separator); + } + + // Crear objeto con todas las traducciones + const combined: Partial> = {}; + + for (const locale of SUPPORTED_LOCALES) { + const parts = strings.map(s => t(s, locale)); + combined[locale] = parts.join(separator); + } + + return combined as I18nTranslations; +} + +// ============================================================================ +// LOCALE METADATA +// ============================================================================ + +/** + * Metadata de locales soportados + */ +export const LOCALE_INFO: Record = { + es: { + code: 'es', + name: 'Spanish', + nativeName: 'Español', + flag: '🇪🇸' + }, + en: { + code: 'en', + name: 'English', + nativeName: 'English', + flag: '🇬🇧' + }, + de: { + code: 'de', + name: 'German', + nativeName: 'Deutsch', + flag: '🇩🇪' + }, + fr: { + code: 'fr', + name: 'French', + nativeName: 'Français', + flag: '🇫🇷' + }, + it: { + code: 'it', + name: 'Italian', + nativeName: 'Italiano', + flag: '🇮🇹' + }, + pt: { + code: 'pt', + name: 'Portuguese', + nativeName: 'Português', + flag: '🇵🇹' + }, + ca: { + code: 'ca', + name: 'Catalan', + nativeName: 'Català', + flag: '🏴' + }, + eu: { + code: 'eu', + name: 'Basque', + nativeName: 'Euskara', + flag: '🏴' + }, + gl: { + code: 'gl', + name: 'Galician', + nativeName: 'Galego', + flag: '🏴' + } +}; + +/** + * Obtener información de un locale + */ +export function getLocaleInfo(locale: SupportedLocale): LocaleInfo { + return LOCALE_INFO[locale]; +} + +// ============================================================================ +// VALIDATION +// ============================================================================ + +/** + * Validar que un I18nString es correcto + */ +export function validateI18nString(text: unknown): text is I18nString { + if (typeof text === 'string') return true; + + if (typeof text !== 'object' || text === null) return false; + + const obj = text as Record; + + // Debe tener al menos el locale por defecto + if (typeof obj[DEFAULT_LOCALE] !== 'string') return false; + + // Todos los valores deben ser strings + return Object.values(obj).every(v => typeof v === 'string'); +} + +/** + * Error para I18nString inválido + */ +export class I18nStringError extends Error { + constructor(message: string) { + super(`[I18n] ${message}`); + this.name = 'I18nStringError'; + } +} + +/** + * Validar y lanzar error si es inválido + */ +export function assertValidI18nString(text: unknown): asserts text is I18nString { + if (!validateI18nString(text)) { + throw new I18nStringError( + `Invalid I18nString. Must be a string or object with at least '${DEFAULT_LOCALE}' key.` + ); + } +} + +// ============================================================================ +// EXPORTS +// ============================================================================ + +export default { + // Constantes y Metadata + SUPPORTED_LOCALES, + + // Utilities de Locale + isSupportedLocale, + getValidLocale, + extractLanguageCode, + getLocaleInfo, + + // Traducción + t, + translate, + hasTranslation, + getAvailableLocales, + isComplete, + getMissingLocales, + + // Builders + createI18nString, + createSimpleI18nString, + expandToTranslations, + combineI18nStrings, + + // Validación y Errores + validateI18nString, + assertValidI18nString, + I18nStringError +}; + diff --git a/srcsa/code/ids.ts b/srcsa/code/ids.ts new file mode 100644 index 0000000..4d0a930 --- /dev/null +++ b/srcsa/code/ids.ts @@ -0,0 +1,19 @@ +// ids.ts +import type {AttrID, ObjectID, OptionID, RuleID, SectionID, HotspotID, ViewID} from "../types/all.ts"; + + + +// ============================================================================ +// EXPORTS +// ============================================================================ + +export const IDS = { + attr : (id: string): AttrID => `at:${id}`, + opt : (id: string): OptionID => `op:${id}`, + sec : (id: string): SectionID=> `sc:${id}`, + obj : (id: string): ObjectID => `ob:${id}`, + rule : (id: string): RuleID => `rl:${id}`, + view : (id: string): ViewID => `vw:${id}`, + hspot: (id: string): HotspotID=> `hs:${id}`, +}; + diff --git a/srcsa/code/vce.ts b/srcsa/code/vce.ts new file mode 100644 index 0000000..d1201d4 --- /dev/null +++ b/srcsa/code/vce.ts @@ -0,0 +1,329 @@ + +import type { + ID, + Section, + Attribute, + JsonLogicEvaluator, + JsonLogic, + Value, AttrID, OptionID, SectionID, ObjectID, RuleID, ViewID, HotspotID +} from '../types/all.ts'; + + + + + + + + + + +// ============================================================================ +// UTILITIES (Con Mejoras) +// ============================================================================ + +/** + * ✅ MEJORADO: Evalúa display flags con soporte para condicionales + */ +export function evaluateDisplayFlag( + flag: boolean | JsonLogic | undefined, + defaultValue: boolean, + attributeValues: Record, Value>, + evaluator: JsonLogicEvaluator +): boolean { + if (flag === undefined) return defaultValue; + if (typeof flag === 'boolean') return flag; + + // Es JsonLogic: evaluar + const data = { + attributes: Object.fromEntries( + Object.entries(attributeValues).map(([id, value]) => [ + id.split(':')[1], // "attr:suelo" → "suelo" + value + ]) + ) + }; + + return evaluator.evaluate(flag, data); +} + +/** + * ✅ MEJORADO: Generación de URL con validación y fallbacks + */ +export function generateSectionImageUrl( + section: Section, + globalAttributes : Record, + sectionAttributes: Record +): string { + const config = section.visualConfig; + + if (!config.imageUrlTemplate) { + console.warn(`[Render] No imageUrlTemplate for section ${section.id}`); + return config.fallbackImage || '/images/fallback.jpg'; + } + + let url = config.imageUrlTemplate; + + // Reemplazar {basePath} + if (config.basePath) { + url = url.replace('{basePath}', config.basePath); + } + + // Reemplazar {sectionId} + const sectionId = section.id.split(':')[1]; + if (sectionId) { + url = url.replace('{sectionId}', sectionId); + } + + /* No overload matches this call. + /The last overload gave the following error. + Argument of type 'string | undefined' is not assignable to parameter of type '(substring: string, ...args: any[]) => string'. + Type 'undefined' is not assignable to type '(substring: string, ...args: any[]) => string'.ts(2769) + lib.es5.d.ts(470, 5): The last overload is declared here. + url = url.replace('{sectionId}', sectionId); + */ + + // Reemplazar {global.attributeId} + config.globalAttributeDependencies?.forEach(attrId => { + const attrName = attrId.split(':')[1]; + const placeholder = `{global.${attrName}}`; + const value = globalAttributes[attrId]; + + if (value !== undefined) { + // Normalizar valor para URL + const normalizedValue = String(value) + .toLowerCase() + .replace(/\s+/g, '_') + .replace(/[^a-z0-9_]/g, ''); + url = url.replace(placeholder, normalizedValue); + } else { + // Atributo faltante: usar 'default' + console.warn(`[Render] Missing global attribute ${attrId}, using 'default'`); + url = url.replace(placeholder, 'default'); + } + }); + + // Reemplazar {section.attributeId} + config.sectionAttributeDependencies?.forEach(attrId => { + const attrName = attrId.split(':')[1]; + const placeholder = `{section.${attrName}}`; + const value = sectionAttributes[attrId]; + + if (value !== undefined) { + const normalizedValue = String(value) + .toLowerCase() + .replace(/\s+/g, '_') + .replace(/[^a-z0-9_]/g, ''); + url = url.replace(placeholder, normalizedValue); + } else { + console.warn(`[Render] Missing section attribute ${attrId}, using 'default'`); + url = url.replace(placeholder, 'default'); + } + }); + + // ✅ NUEVO: Validar que no queden placeholders sin reemplazar + const unresolvedPlaceholders = url.match(/\{[^}]+}/g); + if (unresolvedPlaceholders) { + console.error( + `[Render] Unresolved placeholders in URL: ${unresolvedPlaceholders.join(', ')}`, + `Original template: ${config.imageUrlTemplate}`, + `Partial URL: ${url}` + ); + return config.fallbackImage || '/images/fallback.jpg'; + } + + return url; +} + +/** + * ✅ NUEVO: Generación async de imagen via API + */ +export async function generateSectionImageAsync( + section: Section, + globalAttributes : Record, + sectionAttributes: Record +): Promise { + const config = section.visualConfig; + const fallback = config.fallbackImage || '/images/fallback.jpg'; + + if (config.strategy !== 'api_generated' || !config.apiConfig) { + return generateSectionImageUrl(section, globalAttributes, sectionAttributes); + } + + try { + const params: Record = {}; + config.apiConfig.params?.forEach(attrId => { + const value = globalAttributes[attrId] ?? sectionAttributes[attrId]; + if (value !== undefined) params[attrId] = value; + }); + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), config.apiConfig.timeout || 5000); + + const response = await fetch(config.apiConfig.endpoint, { + method: config.apiConfig.method || 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ sectionId: section.id, attributes: params }), + signal: controller.signal + }); + + clearTimeout(timeoutId); + + // ✅ SOLUCIÓN: En lugar de 'throw', manejamos el error y retornamos el fallback + if (!response.ok) { + console.error(`[Render] API ${config.apiConfig.endpoint} returned ${response.status}`); + return fallback; + } + + const data = await response.json(); + return data.imageUrl || fallback; + + } catch (error) { + // El catch ahora solo se encarga de errores reales: + // fallos de red, JSON malformado o el Timeout (AbortError) + console.error(`[Render] Network or Timeout error for ${section.id}:`, error); + return fallback; + } +} + +/** + * ✅ MEJORADO: Deep equality para valores complejos + */ +export function shouldRerenderSection( + section: Section, + changedAttributeId: AttrID, + oldValue: Value, + newValue: Value, + isGlobalAttribute: boolean +): boolean { + const config = section.visualConfig; + + // Verificar si el atributo afecta a esta sección + const affectsThisSection = isGlobalAttribute + ? config.globalAttributeDependencies?.includes(changedAttributeId) + : config.sectionAttributeDependencies?.includes(changedAttributeId); + + if (!affectsThisSection) return false; + + // Deep equality para valores complejos + return !deepEqual(oldValue, newValue); +} + +/** + * Deep equality check + */ +export function deepEqual(a: Value, b: Value): boolean { + // 1. Chequeo de identidad simple (cubre primitivos iguales) + if (a === b) return true; + + // 2. Si alguno de los dos NO es un objeto o es null, devuelve false + // (Esto descarta string, number, boolean, ID y null de un golpe) + if (typeof a !== 'object' || a === null || typeof b !== 'object' || b === null) { + return false; + } + + // ========================================================= + // AHORA TypeScript SABE que 'a' y 'b' son OBJETOS (Date o Array) + // ========================================================= + + // 3. Ahora es seguro preguntar por Date + if (a instanceof Date && b instanceof Date) { + return a.getTime() === b.getTime(); + } + + // 4. Chequeo de Arrays + if (Array.isArray(a) && Array.isArray(b)) { + if (a.length !== b.length) return false; + return a.every((val, idx) => deepEqual(val, b[idx])); + } + + // 5. Si son objetos pero no Dates ni Arrays, comparamos sus claves (opcional, pero recomendado) + // Esto es útil si tu 'Value' llega a incluir objetos planos en el futuro. + /* + const keysA = Object.keys(a); + const keysB = Object.keys(b); + if (keysA.length !== keysB.length) return false; + return keysA.every(key => deepEqual((a as any)[key], (b as any)[key])); + */ + + return false; +} + +// ============================================================================ +// VALIDATION (Runtime) +// ============================================================================ + +/** + * ✅ NUEVO: Validar modelo en runtime + */ +export function validateAttribute(attr: Attribute): ValidationError[] { + const errors: ValidationError[] = []; + + // Computed debe ser readonly + if (attr.type === 'computed' && !attr.display.readonly) { + errors.push({ + attributeId: attr.id, + message: 'Computed attributes should be readonly', + severity: 'error' + }); + } + + // Fixed no debería afectar precio + if (attr.type === 'fixed' && attr.display.affectsPrice) { + errors.push({ + attributeId: attr.id, + message: 'Fixed attributes should not affect price', + severity: 'warning' + }); + } + + // Verificar que dependencies existen + if (attr.type === 'computed') { + if (!attr.dependencies || attr.dependencies.length === 0) { + errors.push({ + attributeId: attr.id, + message: 'Computed attribute has no dependencies', + severity: 'warning' + }); + } + } + + return errors; +} + +export function validateSection(section: Section): ValidationError[] { + const errors: ValidationError[] = []; + + // Validar que imageUrlTemplate tiene placeholders válidos + if (section.visualConfig.imageUrlTemplate) { + const template = section.visualConfig.imageUrlTemplate; + const placeholders = template.match(/\{[^}]+}/g) || []; + + placeholders.forEach(placeholder => { + const cleanName = placeholder.slice(1, -1); // Remove { } + + if (cleanName.startsWith('global.')) { + const attrName = cleanName.replace('global.', ''); + const attrId = `at:${attrName}` as AttrID; + + if (!section.visualConfig.globalAttributeDependencies?.includes(attrId)) { + errors.push({ + sectionId: section.id, + message: `Placeholder ${placeholder} not in globalAttributeDependencies`, + severity: 'error' + }); + } + } + }); + } + + return errors; +} + +export interface ValidationError { + attributeId?: ID<'at'>; + sectionId?: ID<'sc'>; + message: string; + severity: 'error' | 'warning'; +} + + diff --git a/srcsa/code/view.ts b/srcsa/code/view.ts new file mode 100644 index 0000000..6b0ca39 --- /dev/null +++ b/srcsa/code/view.ts @@ -0,0 +1,109 @@ +// view.ts + +import type { Hotspot, SectionView, AttrID, SectionID} from "../types/all.ts"; + +// ============================================================================ +// UTILITIES +// ============================================================================ + +/** + * Obtener la vista por defecto de una lista de vistas + */ +export function getDefaultView(views: SectionView[]): SectionView | null { + const defaultView = views.find(v => v.isDefault === true); + return defaultView || views[0] || null; +} + +/** + * Filtrar vistas visibles según condiciones + */ +export function getVisibleViews( + views: SectionView[], + attributeValues: Record, + evaluator: any +): SectionView[] { + return views.filter(view => { + if (view.visible === undefined || view.visible === true) { + return true; + } + if (view.visible === false) { + return false; + } + // Es JsonLogic, evaluar + const data = { attributes: attributeValues }; + return evaluator.evaluate(view.visible, data); + }); +} + +/** + * Obtener hotspots visibles de una vista + */ +export function getVisibleHotspots( + view: SectionView, + attributeValues: Record, + evaluator: any +): Hotspot[] { + if (!view.hotspots) return []; + + return view.hotspots.filter(hotspot => { + if (hotspot.visible === undefined || hotspot.visible === true) { + return true; + } + if (hotspot.visible === false) { + return false; + } + // Es JsonLogic, evaluar + const data = { attributes: attributeValues }; + return evaluator.evaluate(hotspot.visible, data); + }); +} + +/** + * Generar URL de imagen para una vista específica + */ +export function generateViewImageUrl( + view: SectionView, + sectionId: SectionID, + globalAttributes : Record, + sectionAttributes: Record +): string { + const config = view.visualConfig; + + if (!config.imageUrlTemplate) { + return config.fallbackImage || '/images/fallback.jpg'; + } + + let url = config.imageUrlTemplate; + + // Reemplazar {basePath} + if (config.basePath) { + url = url.replace('{basePath}', config.basePath); + } + + // Reemplazar {sectionId} + url = url.replace('{sectionId}', sectionId.split(':')[1]); + + // Reemplazar {viewId} + url = url.replace('{viewId}', view.id.split(':')[1]); + + // Reemplazar atributos globales + config.globalAttributeDependencies?.forEach(attrId => { + const attrName = attrId.split(':')[1]; + const value = globalAttributes[attrId]; + if (value !== undefined) { + url = url.replace(`{global.${attrName}}`, String(value)); + } + }); + + // Reemplazar atributos de sección + config.sectionAttributeDependencies?.forEach(attrId => { + const attrName = attrId.split(':')[1]; + const value = sectionAttributes[attrId]; + if (value !== undefined) { + url = url.replace(`{section.${attrName}}`, String(value)); + } + }); + + return url; +} + diff --git a/srcsa/index.ts b/srcsa/index.ts new file mode 100644 index 0000000..954db84 --- /dev/null +++ b/srcsa/index.ts @@ -0,0 +1,107 @@ +import type { ConfigurableObject, Section, DynamicAttribute, Attribute } from './types/model.ts'; +import { IDS, JsonLogicEvaluator } from './types/model.ts'; +import { evaluateDisplayFlag, generateSectionImageUrl } from './code/vce.ts'; + +// ------------------------------- +// 1️⃣ Crear catálogo de opciones +// ------------------------------- +const catalogOptions = { + [IDS.opt('rojo')]: { id: IDS.opt('rojo'), name: 'Rojo', description: 'Color Rojo' }, + [IDS.opt('blanco')]: { id: IDS.opt('blanco'), name: 'Blanco', description: 'Color Blanco' }, + [IDS.opt('beige')]: { id: IDS.opt('beige'), name: 'Beige', description: 'Interior Beige' } +}; + +// ------------------------------- +// 2️⃣ Crear atributos +// ------------------------------- +const colorAttr: DynamicAttribute = { + id: IDS.attr('color'), + name: 'Color Exterior', + description: 'Color de la carrocería', + type: 'dynamic', + dataType: 'reference', + defaultValue: IDS.opt('blanco'), + options: [ + { optionId: IDS.opt('rojo') }, + { optionId: IDS.opt('blanco') } + ], + display: { + uiVisible: true, + affectsVisual: true, + affectsPrice: true + } +}; + +const interiorAttr: DynamicAttribute = { + id: IDS.attr('interior'), + name: 'Interior', + description: 'Color del interior', + type: 'dynamic', + dataType: 'reference', + defaultValue: IDS.opt('beige'), + options: [ + { optionId: IDS.opt('beige') }, + { optionId: IDS.opt('blanco') } + ], + display: { + uiVisible: true, + affectsVisual: true, + affectsPrice: true + } +}; + +// ------------------------------- +// 3️⃣ Crear sección +// ------------------------------- +const salonSection: Section = { + id: IDS.sec('salon'), + name: 'Salón', + description: 'Sección principal', + attributes: [interiorAttr], + availability: { mode: 'required' }, + visualConfig: { + strategy: 'dynamic_image', + imageUrlTemplate: '/images/{sectionId}_{global.color}_{section.interior}.jpg', + basePath: '/images', + globalAttributeDependencies: [colorAttr.id], + sectionAttributeDependencies: [interiorAttr.id], + fallbackImage: '/images/fallback.jpg' + } +}; + +// ------------------------------- +// 4️⃣ Crear objeto configurable +// ------------------------------- +const coche: ConfigurableObject = { + id: IDS.obj('tesla_model3'), + name: 'Tesla Model 3', + description: 'Ejemplo de coche configurador', + attributes: [colorAttr], + sections: { + [salonSection.id]: salonSection + } +}; + +// ------------------------------- +// 5️⃣ Estado del usuario +// ------------------------------- +const userGlobalAttrs = { + [colorAttr.id]: IDS.opt('rojo') +}; +const userSectionAttrs = { + [interiorAttr.id]: IDS.opt('beige') +}; + +// ------------------------------- +// 6️⃣ Evaluar display flags +// ------------------------------- +const evaluator = new JsonLogicEvaluator(); +const isColorVisible = evaluateDisplayFlag(colorAttr.display.uiVisible, true, userGlobalAttrs, evaluator); + +console.log('Color visible?', isColorVisible); + +// ------------------------------- +// 7️⃣ Generar URL de imagen de sección +// ------------------------------- +const imageUrl = generateSectionImageUrl(salonSection, userGlobalAttrs, userSectionAttrs); +console.log('Imagen generada:', imageUrl); \ No newline at end of file diff --git a/srcsa/tests/view_examples.ts b/srcsa/tests/view_examples.ts new file mode 100644 index 0000000..6475ebb --- /dev/null +++ b/srcsa/tests/view_examples.ts @@ -0,0 +1,217 @@ +// view_examples.ts +import type {AttrID, HotspotID, SectionView, ViewID} from '../types/all.ts'; + +// ============================================================================ +// EJEMPLOS DE USO +// ============================================================================ + +/** + * EJEMPLO 1: Salón con 3 vistas + */ +export const ejemploSalonVistas: SectionView[] = [ + // Vista 1: Frontal (por defecto) + { + id: "vw:salon_frontal" as ViewID, + name: { es: "Vista Frontal", en: "Front View" }, + description: { es: "Vista principal del salón" }, + type: "front", + isDefault: true, + order: 1, + + visualConfig: { + strategy: "dynamic_image", + basePath: "/images/sections/salon", + imageUrlTemplate: "{basePath}/frontal_{global.suelo}_{global.zocalo}_{section.mobiliario}.jpg", + globalAttributeDependencies: [ + "at:suelo" as AttrID, + "at:zocalo" as AttrID + ], + sectionAttributeDependencies: [ + "at:mobiliario_salon" as AttrID, + "at:iluminacion_salon" as AttrID + ] + }, + + // Hotspots en vista frontal + hotspots: [ + { + id: "hs:salon_suelo" as HotspotID, + name: { es: "Suelo" }, + x: 50, + y: 85, + radius: 30, + color: "#FFD700", + icon: "floor", + scopeType: "global", + targetAttributeId: "at:suelo" as AttrID, + tooltip: { es: "Configurar tipo de suelo" }, + zIndex: 1 + }, + { + id: "hs:salon_muebles" as HotspotID, + name: { es: "Mobiliario" }, + x: 40, + y: 50, + radius: 40, + color: "#4CAF50", + icon: "sofa", + scopeType: "section", + targetAttributeId: "at:mobiliario_salon" as AttrID, + tooltip: { es: "Elegir mobiliario del salón" }, + zIndex: 2 + }, + { + id: "hs:salon_lampara" as HotspotID, + name: { es: "Iluminación" }, + x: 50, + y: 15, + radius: 25, + color: "#FFC107", + icon: "lightbulb", + scopeType: "section", + targetAttributeId: "at:iluminacion_salon" as AttrID, + tooltip: { es: "Configurar iluminación" }, + zIndex: 3 + } + ] + }, + + // Vista 2: Lateral + { + id: "vw:salon_lateral" as ViewID, + name: { es: "Vista Lateral", en: "Side View" }, + type: "side", + order: 2, + + visualConfig: { + strategy: "dynamic_image", + basePath: "/images/sections/salon", + imageUrlTemplate: "{basePath}/lateral_{global.suelo}_{section.mobiliario}.jpg", + globalAttributeDependencies: ["at:suelo" as AttrID], + sectionAttributeDependencies: ["at:mobiliario_salon" as AttrID] + }, + + // Esta vista tiene menos hotspots + hotspots: [ + { + id: "hs:salon_lateral_suelo" as HotspotID, + name: { es: "Suelo" }, + x: 50, + y: 90, + radius: 30, + scopeType: "global", + targetAttributeId: "at:suelo" as AttrID + } + ] + }, + + // Vista 3: Cenital (sin hotspots) + { + id: "vw:salon_cenital" as ViewID, + name: { es: "Plano", en: "Floor Plan" }, + type: "top", + order: 3, + + visualConfig: { + strategy: "static_image", + basePath: "/images/sections/salon", + imageUrlTemplate: "{basePath}/plano.jpg" + } + + // Sin hotspots - solo visualización + } +]; + +/** + * EJEMPLO 2: Coche con vista 360° + */ +export const ejemploCocheVista360: SectionView = { + id: "vw:coche_360" as ViewID, + name: { es: "Vista 360°" }, + type: "panoramic", + isDefault: true, + + visualConfig: { + strategy: "three_d", + threeDConfig: { + modelUrl: "/models/coche.glb", + cameraPosition: { x: 5, y: 2, z: 5 }, + textureAttributes: [ + { + attributeId: "at:color" as AttrID, + scope: "global" + }, + { + attributeId: "at:llantas" as AttrID, + scope: "section" + } + ] + } + }, + + // Hotspots en modelo 3D (coordenadas 3D) + hotspots: [ + { + id: "hs:puerta" as HotspotID, + name: { es: "Puerta" }, + x: 30, // Coordenadas relativas al canvas + y: 50, + scopeType: "section", + targetAttributeId: "at:tipo_puerta" as AttrID + } + ] +}; + +/** + * EJEMPLO 3: Vista condicional (solo visible con cierta opción) + */ +export const ejemploVistaCondicional: SectionView = { + id: "vw:terraza_exterior" as ViewID, + name: { es: "Vista Exterior" }, + type: "front", + + // Solo visible si tiene_terraza = true + visible: { + "==": [ + { "var": "attributes.tiene_terraza" }, + "opt:si" + ] + }, + + visualConfig: { + strategy: "dynamic_image", + imageUrlTemplate: "/images/terraza_exterior_{section.suelo_terraza}.jpg", + sectionAttributeDependencies: ["at:suelo_terraza" as AttrID] + } +}; + +/** + * EJEMPLO 4: Vista de detalle con zoom + */ +export const ejemploVistaDetalle: SectionView = { + id: "vw:cocina_encimera_detalle" as ViewID, + name: { es: "Detalle Encimera" }, + type: "detail", + + visualConfig: { + strategy: "dynamic_image", + imageUrlTemplate: "/images/cocina/detalle_encimera_{section.encimera}.jpg", + sectionAttributeDependencies: ["at:encimera" as AttrID] + }, + + // Hotspot específico de la encimera + hotspots: [ + { + id: "hs:encimera_material" as HotspotID, + name: { es: "Material Encimera" }, + x: 50, + y: 50, + radius: 50, + scopeType: "section", + targetAttributeId: "at:encimera" as AttrID, + tooltip: { es: "Cambiar material de la encimera" } + } + ] +}; + + diff --git a/srcsa/types/all.ts b/srcsa/types/all.ts new file mode 100644 index 0000000..4cdb9e0 --- /dev/null +++ b/srcsa/types/all.ts @@ -0,0 +1,7 @@ +// types.ts + +export * from './primitives.ts'; +export * from './i18n.ts'; +export * from './json_logic.ts'; +export * from './view.ts'; +export * from './vce_model.ts'; diff --git a/srcsa/types/i18n.ts b/srcsa/types/i18n.ts new file mode 100644 index 0000000..c1abd41 --- /dev/null +++ b/srcsa/types/i18n.ts @@ -0,0 +1,93 @@ +// ui18.ts + +export type SupportedLocale = + | 'es' // Español + | 'en' // English + | 'de' // Deutsch + | 'fr' // Français + | 'it' // Italiano + | 'pt' // Português + | 'ca' // Català + | 'eu' // Euskara + | 'gl'; // Galego + + + +/** + * Locale por defecto del sistema + */ +export const DEFAULT_LOCALE: SupportedLocale = 'es'; + +/** + * Lista de todos los locales soportados + */ +export const SUPPORTED_LOCALES: readonly SupportedLocale[] = [ + 'es', + 'en', + 'de', + 'fr', + 'it', + 'pt', + 'ca', + 'eu', + 'gl' +] as const; + + +// ============================================================================ +// I18N STRING TYPES +// ============================================================================ + +/** + * Objeto con traducciones tipadas + * Garantiza que al menos existe el locale por defecto + */ +export type I18nTranslations = { + [K in SupportedLocale]?: string; +} & { + [DEFAULT_LOCALE]: string; // Español es obligatorio +}; + + + +/** + * String internacionalizable + * + * Puede ser: + * - `string`: Texto simple (mismo en todos los idiomas) + * - `I18nTranslations`: Objeto con traducciones por locale + * + * @example + * // Simple (mismo en todos los idiomas) + * const label: I18nString = "OK"; + * + * // Con traducciones + * const greeting: I18nString = { + * es: "Hola", + * en: "Hello", + * de: "Hallo" + * }; + * + * // Solo español (obligatorio) + algún otro + * const name: I18nString = { + * es: "Configuración", + * en: "Configuration" + * }; + */ +export type I18nString = string | I18nTranslations; + + +// ============================================================================ +// LOCALE METADATA +// ============================================================================ + +/** + * Información sobre un locale + */ +export interface LocaleInfo { + code: SupportedLocale; + name: string; + nativeName: string; + flag: string; + rtl?: boolean; +} diff --git a/srcsa/types/json_logic.ts b/srcsa/types/json_logic.ts new file mode 100644 index 0000000..fffd61b --- /dev/null +++ b/srcsa/types/json_logic.ts @@ -0,0 +1,59 @@ +// json_logic.ts + + + +// ============================================================================ +// JSON LOGIC +// ============================================================================ + +export type JsonLogic = + | { var: string } + | { [op: string]: any } + | string + | number + | boolean + | null; + + + +// ============================================================================ +// JSONLOGIC EVALUATOR (Simplificado) +// ============================================================================ + +export class JsonLogicEvaluator { + evaluate(expression: JsonLogic, data: Record): any { + // Implementación completa en archivo separado + // Por ahora: placeholder + if (typeof expression === 'boolean') return expression; + if (typeof expression === 'number') return expression; + if (typeof expression === 'string') return expression; + + if (typeof expression === 'object' && expression !== null) { + if ('var' in expression) { + return this.getVar(expression.var, data); + } + if ('==' in expression) { + const [a, b] = (expression as any)['==']; + return this.evaluate(a, data) === this.evaluate(b, data); + } + if ('if' in expression) { + const [cond, then_, else_] = (expression as any)['if']; + return this.evaluate(cond, data) + ? this.evaluate(then_, data) + : this.evaluate(else_, data); + } + } + + return expression; + } + + private getVar(path: string, data: Record): any { + const parts = path.split('.'); + let current = data; + for (const part of parts) { + if (current === undefined || current === null) return undefined; + current = current[part]; + } + return current; + } +} \ No newline at end of file diff --git a/srcsa/types/metadata.ts b/srcsa/types/metadata.ts new file mode 100644 index 0000000..edb9637 --- /dev/null +++ b/srcsa/types/metadata.ts @@ -0,0 +1,6 @@ +// metadata.ts + + + +export type Metadata = Record; + diff --git a/srcsa/types/primitives.ts b/srcsa/types/primitives.ts new file mode 100644 index 0000000..899cef8 --- /dev/null +++ b/srcsa/types/primitives.ts @@ -0,0 +1,23 @@ +// primitives.ts + + +export type ID = `${T}:${string}`; + + +export type Value = + | string + | number + | boolean + | Date + | ID + | Value[]; + +export type DataType = + | 'string' + | 'number' + | 'boolean' + | 'date' + | 'currency' + | 'dimension' + | 'reference' + | 'list'; \ No newline at end of file diff --git a/srcsa/types/vce_model.ts b/srcsa/types/vce_model.ts new file mode 100644 index 0000000..58ec498 --- /dev/null +++ b/srcsa/types/vce_model.ts @@ -0,0 +1,362 @@ +/** + * ============================================================================ + * CONFIGURATION MODEL - FINAL VERSION + * ============================================================================ + * + * Mejoras implementadas: + * + * ✅ NIVEL 1 (Incrementales): + * 1. Condicionales en display flags (uiVisible, affectsVisual, affectsPrice) + * 2. Validación y fallbacks en generateImageUrl + * 3. Caching en utilities + * 4. Validación de modelo en runtime + * + * ✅ NIVEL 2 (Intermedias): + * 5. Soporte async rendering (API-generated) + * 6. Deep equality para shouldRerender + * 7. Logging de visual changes + * + * ============================================================================ + */ +import type { JsonLogic } from "./json_logic.ts"; +import type { I18nString } from "./i18n.ts"; +import type { Metadata } from "./metadata.ts"; +import type { RenderingStrategy } from "./view.ts" +import type { ID, Value, DataType } from "./primitives.ts"; + + + +export type AttrID = ID<'at'>; +export type SectionID = ID<'sc'>; +export type OptionID = ID<'op'>; +export type ObjectID = ID<'ob'>; +export type RuleID = ID<'rl'>; + + + +// ============================================================================ +// ATTRIBUTE DISPLAY (Con Condicionales) +// ============================================================================ + +/** + * ✅ MEJORADO: Display flags pueden ser boolean O expresión condicional + */ +export interface AttributeDisplay { + /** + * Si se muestra en UI + * - boolean: true/false estático + * - JsonLogic: Evaluado dinámicamente + * + * Ejemplo condicional: + * { + * "if": [ + * { "==": [{ "var": "attributes.usuario_tipo" }, "avanzado"] }, + * true, + * false + * ] + * } + */ + uiVisible?: boolean | JsonLogic; + + /** + * Si afecta a la visualización + * - boolean: true/false estático + * - JsonLogic: Condicional + * + * Ejemplo: Solo afecta visual si plan es premium + * { + * "==": [{ "var": "attributes.plan" }, "premium"] + * } + */ + affectsVisual?: boolean | JsonLogic; + + /** + * Si afecta al precio + */ + affectsPrice?: boolean | JsonLogic; + + /** + * Si es de solo lectura + */ + readonly?: boolean; + + /** + * Orden de visualización + */ + order?: number; + + /** + * Icono + */ + icon?: string; + + /** + * Texto de ayuda + */ + helpText?: I18nString; +} + +// ============================================================================ +// BASE ATTRIBUTE +// ============================================================================ + +export interface BaseAttribute { + id: AttrID; + name: I18nString; + description: I18nString; + type: AttributeType; + category?: AttributeCategory; + display: AttributeDisplay; + metadata?: Metadata; +} + +export type AttributeType = 'fixed' | 'dynamic' | 'quantifiable' | 'computed'; +export type AttributeCategory = 'physical' | 'aesthetic' | 'functional' | 'structural' | 'technical' | 'financial'; + +// ============================================================================ +// ATTRIBUTE TYPES +// ============================================================================ + +export interface FixedAttribute extends BaseAttribute { + type: 'fixed'; + dataType: DataType; + value: Value; + unit?: I18nString; +} + +export interface DynamicAttribute extends BaseAttribute { + type: 'dynamic'; + dataType: 'reference'; + defaultValue: ID<'op'>; + options: AttributeOption[]; + required?: boolean; + isController?: boolean; + controls?: ID<'sc'>[]; + filterExpression?: JsonLogic; +} + +export interface QuantifiableAttribute extends BaseAttribute { + type: 'quantifiable'; + dataType: 'number'; + quantity: QuantityDefinition; + unit?: I18nString; + userConfigurable?: boolean; + pricePerUnit?: number; + min?: number; + max?: number; +} + +export interface ComputedAttribute extends BaseAttribute { + type: 'computed'; + dataType: DataType; + unit?: I18nString; + expression: JsonLogic; + dependencies: ID<'at'>[]; +} + +export type Attribute = + | FixedAttribute + | DynamicAttribute + | QuantifiableAttribute + | ComputedAttribute; + +export interface AttributeOption { + optionId: OptionID; + priority?: number; + pricingOverride?: number; + metadata?: Metadata; +} + +export type QuantityDefinition = + | number + | { min: number; max: number; step?: number; default: number } + | { values: number[]; default: number }; + +// ============================================================================ +// VISUAL RENDERING (Con Async Support) +// ============================================================================ + +export interface SectionVisualConfig { + strategy: RenderingStrategy; + + /** Template para URL estática */ + imageUrlTemplate?: string; + + /** Atributos globales que afectan la imagen */ + globalAttributeDependencies?: AttrID[]; + + /** Atributos de sección que afectan la imagen */ + sectionAttributeDependencies?: AttrID[]; + + /** Ruta base */ + basePath?: string; + + /** ✅ NUEVO: Config para generación async via API */ + apiConfig?: { + endpoint: string; + method?: 'GET' | 'POST'; + params?: AttrID[]; + timeout?: number; + }; + + /** Config 3D */ + threeDConfig?: { + modelUrl: string; + textureAttributes: Array<{ + attributeId: AttrID; + scope: 'global' | 'section'; + }>; + }; + + /** ✅ NUEVO: Imagen de fallback si falla generación */ + fallbackImage?: string; +} + + +// ============================================================================ +// SECTION & OBJECT +// ============================================================================ + +export interface Section { + id: SectionID; + name: I18nString; + description: I18nString; + attributes: Attribute[]; + visualConfig: SectionVisualConfig; + availability: SectionAvailability; + order?: number; + icon?: string; + metadata?: Metadata; +} + +export interface SectionAvailability { + mode: 'required' | 'optional' | 'conditional'; + condition?: JsonLogic; + dependsOn?: ID<'attr'>[]; + onDeactivate?: { + action: 'clear' | 'preserve' | 'reset'; + confirmWithUser?: boolean; + confirmMessage?: I18nString; + }; +} + +export interface ConfigurableObject { + id: ObjectID; + name: I18nString; + description: I18nString; + attributes: Attribute[]; + sections: Record; + sectionOrder?: ID<'sec'>[]; + category?: string; + metadata?: Metadata; +} + +// ============================================================================ +// OPTION DEFINITION +// ============================================================================ + +export interface OptionDefinition { + id: OptionID; + name: I18nString; + description: I18nString; + media?: { + thumbnail?: string; + image?: string; + icon?: string; + gallery?: string[]; + }; + pricing?: { + baseAmount: number | 'consultation'; + currency?: string; + dynamicExpression?: JsonLogic; + }; + tags?: string[]; + style?: string; + metadata?: Metadata; +} + +// ============================================================================ +// CATALOG +// ============================================================================ + +export interface ConfigurationCatalog { + name: I18nString; + description?: I18nString; + supportedLocales: string[]; + defaultLocale: string; + options: Record; + objects: Record; + rules? : Record; +} + +export interface ValidationRule { + id: RuleID; + name: I18nString; + condition: JsonLogic; + action: { + type: 'allow' | 'forbid' | 'require' | 'suggest'; + targetAttribute: ID<'attr'>; + values: Value[]; + }; + priority: number; + bidirectional: boolean; + affects: AttrID[]; + severity: 'error' | 'warning' | 'info'; + message: I18nString; +} + + + + +// ============================================================================ +// CACHING UTILITIES +// ============================================================================ + +/** + * ✅ NUEVO: Cache para utilities + */ +export class AttributeCache { + private uiVisibleCache = new Map, boolean>(); + private visualAttributesCache: Attribute[] | null = null; + private priceAttributesCache: Attribute[] | null = null; + + invalidate(): void { + this.uiVisibleCache.clear(); + this.visualAttributesCache = null; + this.priceAttributesCache = null; + } + + getUIVisible( + attrId: ID<'attr'>, + compute: () => boolean + ): boolean { + if (this.uiVisibleCache.has(attrId)) { + return this.uiVisibleCache.get(attrId)!; + } + const result = compute(); + this.uiVisibleCache.set(attrId, result); + return result; + } + + getVisualAttributes( + compute: () => Attribute[] + ): Attribute[] { + if (this.visualAttributesCache) { + return this.visualAttributesCache; + } + this.visualAttributesCache = compute(); + return this.visualAttributesCache; + } + + getPriceAttributes( + compute: () => Attribute[] + ): Attribute[] { + if (this.priceAttributesCache) { + return this.priceAttributesCache; + } + this.priceAttributesCache = compute(); + return this.priceAttributesCache; + } +} + + diff --git a/srcsa/types/view.ts b/srcsa/types/view.ts new file mode 100644 index 0000000..a80b21d --- /dev/null +++ b/srcsa/types/view.ts @@ -0,0 +1,189 @@ +// view.ts +/** + * ============================================================================ + * CONFIGURATION VIEWS - Visual Representation + * ============================================================================ + * + * CONCEPTO: + * + * Una SECCIÓN puede tener MÚLTIPLES VISTAS + * + * Ejemplo: Sección "Salón" + * - Vista "Frontal": Muestra pared frontal con ventanas y muebles + * - Vista "Lateral": Muestra pared lateral con puerta + * - Vista "Cenital": Vista desde arriba, muestra distribución + * - Vista "360": Vista panorámica + * + * Cada vista tiene: + * - Su propia imagen base + * - Sus propios hotspots (puntos interactivos) + * - Configuración de cómo se genera la URL de imagen + * + * ============================================================================ + */ + +import type { ID } from './primitives.ts'; +import type { I18nString } from './i18n'; +import type { JsonLogic } from "./json_logic.ts"; +import type {Attribute, AttrID} from "./vce_model.ts"; + + + + +// ============================================================================ +// VIEW TYPES (Tipos de Vista) +// ============================================================================ + + +export type ViewID = ID<'vw'>; +export type HotspotID = ID<'hs'>; + +/** + * Tipos de vista predefinidos + */ +type ViewType = + | 'front' // Vista frontal + | 'side' // Vista lateral + | 'rear' // Vista posterior + | 'top' // Vista desde arriba + | 'isometric' // Vista isométrica/3D + | 'panoramic' // Vista 360° panorámica + | 'detail' // Vista de detalle/zoom + | 'blueprint' // Plano/blueprint + | 'custom'; // Tipo personalizado + +// ============================================================================ +// HOTSPOT (Punto Interactivo) +// ============================================================================ + +/** + * Punto interactivo en una vista + * Al hacer click, permite configurar un atributo específico + */ +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 (hex, rgb, etc.) */ + 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 (Vista Visual de una Sección) +// ============================================================================ + +/** + * Vista visual de una sección + * Una sección puede tener múltiples vistas + */ +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; +} + +/** + * Configuración visual específica de una vista + */ +export interface ViewVisualConfig { + /** Estrategia de renderizado */ + strategy: RenderingStrategy; + + /** Template para URL de imagen */ + imageUrlTemplate?: string; + + /** Ruta base de imágenes */ + basePath?: string; + + /** Atributos globales que afectan a esta vista */ + globalAttributeDependencies?: AttrID[]; + + /** Atributos de sección que afectan a esta vista */ + sectionAttributeDependencies?: AttrID[]; + + /** Imagen de fallback */ + fallbackImage?: string; + + /** Config para API */ + apiConfig?: { + endpoint: string; + method?: 'GET' | 'POST'; + params?: AttrID[]; + timeout?: number; + }; + + /** Config 3D */ + threeDConfig?: { + modelUrl: string; + cameraPosition?: { x: number; y: number; z: number }; + textureAttributes?: Array<{ + attributeId: AttrID; + scope: 'global' | 'section'; + }>; + }; +} + +export type RenderingStrategy = + | 'static_image' + | 'dynamic_image' + | 'api_generated' + | 'three_d' + | 'composite_layers' + | 'none'; + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2fd2273 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "baseUrl": "./src", + "paths": { + "@/types/*": ["types/*"], + "@/utils/*": ["utils/*"], + "@/engine/*": ["engine/*"], + "@/constants/*": ["constants/*"], + "@/*": ["*"] + }, + + // ✅ Opciones CRÍTICAS para usar .ts en imports: + "allowImportingTsExtensions": true, + "noEmit": true, // Obligatorio con la opción anterior + "module": "esnext", // Cambiado de nodenext a esnext + "moduleResolution": "bundler", // Permite resoluciones modernas tipo Vite/Bun + + "target": "esnext", + "strict": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "skipLibCheck": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}