Merge branch 'fix/merge' into feat/ai-generate-plan

This commit is contained in:
2026-01-21 16:03:22 -06:00
20 changed files with 3770 additions and 2079 deletions

268
bun.lock
View File

@@ -4,12 +4,9 @@
"": { "": {
"name": "acad-ia-2", "name": "acad-ia-2",
"dependencies": { "dependencies": {
"@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-avatar": "^1.1.11", "@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8", "@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-scroll-area": "^1.2.10",
@@ -19,7 +16,6 @@
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@stepperize/react": "^5.1.9", "@stepperize/react": "^5.1.9",
"@supabase/supabase-js": "^2.90.1",
"@tailwindcss/vite": "^4.0.6", "@tailwindcss/vite": "^4.0.6",
"@tanstack/react-devtools": "^0.7.0", "@tanstack/react-devtools": "^0.7.0",
"@tanstack/react-query": "^5.66.5", "@tanstack/react-query": "^5.66.5",
@@ -30,7 +26,6 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.562.0", "lucide-react": "^0.562.0",
"motion": "^12.24.7", "motion": "^12.24.7",
"react": "^19.2.0", "react": "^19.2.0",
@@ -38,14 +33,12 @@
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0",
"tailwindcss": "^4.0.6", "tailwindcss": "^4.0.6",
"tw-animate-css": "^1.3.6", "tw-animate-css": "^1.3.6",
"use-debounce": "^10.1.0",
}, },
"devDependencies": { "devDependencies": {
"@tanstack/devtools-vite": "^0.3.11", "@tanstack/devtools-vite": "^0.3.11",
"@tanstack/eslint-config": "^0.3.0", "@tanstack/eslint-config": "^0.3.0",
"@testing-library/dom": "^10.4.0", "@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0", "@testing-library/react": "^16.2.0",
"@types/bun": "^1.3.6",
"@types/node": "^22.10.2", "@types/node": "^22.10.2",
"@types/react": "^19.2.0", "@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0", "@types/react-dom": "^19.2.0",
@@ -59,7 +52,6 @@
"jsdom": "^27.0.0", "jsdom": "^27.0.0",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.7.2", "prettier-plugin-tailwindcss": "^0.7.2",
"supabase": "^2.72.2",
"typescript": "^5.7.2", "typescript": "^5.7.2",
"vite": "^7.1.7", "vite": "^7.1.7",
"vitest": "^3.0.5", "vitest": "^3.0.5",
@@ -68,7 +60,7 @@
}, },
}, },
"packages": { "packages": {
"@acemir/cssom": ["@acemir/cssom@0.9.31", "", {}, "sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA=="], "@acemir/cssom": ["@acemir/cssom@0.9.30", "", {}, "sha512-9CnlMCI0LmCIq0olalQqdWrJHPzm0/tw3gzOA9zJSgvFX7Xau3D24mAGa4BtwxwY69nsuJW6kQqqCzf/mEcQgg=="],
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.1.1", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "lru-cache": "^11.2.4" } }, "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ=="], "@asamuzakjp/css-color": ["@asamuzakjp/css-color@4.1.1", "", { "dependencies": { "@csstools/css-calc": "^2.1.4", "@csstools/css-color-parser": "^3.1.0", "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4", "lru-cache": "^11.2.4" } }, "sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ=="],
@@ -76,23 +68,23 @@
"@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="], "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
"@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
"@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="],
"@babel/core": ["@babel/core@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw=="], "@babel/core": ["@babel/core@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", "@babel/helpers": "^7.28.4", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw=="],
"@babel/generator": ["@babel/generator@7.28.6", "", { "dependencies": { "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw=="], "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], "@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.28.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
@@ -100,25 +92,25 @@
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
"@babel/helpers": ["@babel/helpers@7.28.6", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw=="], "@babel/helpers": ["@babel/helpers@7.28.4", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.28.4" } }, "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w=="],
"@babel/parser": ["@babel/parser@7.28.6", "", { "dependencies": { "@babel/types": "^7.28.6" }, "bin": "./bin/babel-parser.js" }, "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ=="], "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="],
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w=="],
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="], "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ=="],
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="], "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="], "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
"@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], "@babel/runtime": ["@babel/runtime@7.28.4", "", {}, "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ=="],
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], "@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
"@babel/traverse": ["@babel/traverse@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.6", "@babel/template": "^7.28.6", "@babel/types": "^7.28.6", "debug": "^4.3.1" } }, "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg=="], "@babel/traverse": ["@babel/traverse@7.28.5", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "debug": "^4.3.1" } }, "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ=="],
"@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="],
"@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="],
@@ -128,7 +120,7 @@
"@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="], "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="],
"@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.0.25", "", {}, "sha512-g0Kw9W3vjx5BEBAF8c5Fm2NcB/Fs8jJXh85aXqwEXiL+tqtOut07TWgyaGzAAfTM+gKckrrncyeGEZPcaRgm2Q=="], "@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.0.22", "", {}, "sha512-qBcx6zYlhleiFfdtzkRgwNC7VVoAwfK76Vmsw5t+PbvtdknO9StgRk7ROvq9so1iqbdW4uLIDAsXRsTfUrIoOw=="],
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
@@ -230,8 +222,6 @@
"@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="],
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
@@ -248,21 +238,17 @@
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
"@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="],
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
"@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.11", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q=="], "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.11", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q=="],
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="], "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="],
"@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="],
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], "@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="],
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="], "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
@@ -270,8 +256,6 @@
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="], "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
"@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="],
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="], "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="], "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
@@ -280,8 +264,6 @@
"@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="], "@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="],
"@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="], "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="], "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
@@ -290,12 +272,10 @@
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="], "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="], "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
@@ -398,19 +378,7 @@
"@stepperize/react": ["@stepperize/react@5.1.9", "", { "dependencies": { "@stepperize/core": "1.2.7" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-yBgw1I5Tx6/qZB4xTdVBaPGfTqH5aYS1WFB5vtR8+fwPeqd3YNuOnQ1pJM6w/xV/gvryuy31hbFw080lZc+/hw=="], "@stepperize/react": ["@stepperize/react@5.1.9", "", { "dependencies": { "@stepperize/core": "1.2.7" }, "peerDependencies": { "react": "^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-yBgw1I5Tx6/qZB4xTdVBaPGfTqH5aYS1WFB5vtR8+fwPeqd3YNuOnQ1pJM6w/xV/gvryuy31hbFw080lZc+/hw=="],
"@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.7.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/types": "^8.52.0", "eslint-visitor-keys": "^5.0.0", "espree": "^11.0.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-PsSugIf9ip1H/mWKj4bi/BlEoerxXAda9ByRFsYuwsmr6af9NxJL0AaiNXs8Le7R21QR5KMiD/KdxZZ71LjAxQ=="], "@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.6.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.0", "@typescript-eslint/types": "^8.47.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": ">=9.0.0" } }, "sha512-JCs+MqoXfXrRPGbGmho/zGS/jMcn3ieKl/A8YImqib76C8kjgZwq5uUFzc30lJkMvcchuRn6/v8IApLxli3Jyw=="],
"@supabase/auth-js": ["@supabase/auth-js@2.90.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-vxb66dgo6h3yyPbR06735Ps+dK3hj0JwS8w9fdQPVZQmocSTlKUW5MfxSy99mN0XqCCuLMQ3jCEiIIUU23e9ng=="],
"@supabase/functions-js": ["@supabase/functions-js@2.90.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-x9mV9dF1Lam9qL3zlpP6mSM5C9iqMPtF5B/tU1Jj/F0ufX5mjDf9ghVBaErVxmrQJRL4+iMKWKY2GnODkpS8tw=="],
"@supabase/postgrest-js": ["@supabase/postgrest-js@2.90.1", "", { "dependencies": { "tslib": "2.8.1" } }, "sha512-jh6vqzaYzoFn3raaC0hcFt9h+Bt+uxNRBSdc7PfToQeRGk7PDPoweHsbdiPWREtDVTGKfu+PyPW9e2jbK+BCgQ=="],
"@supabase/realtime-js": ["@supabase/realtime-js@2.90.1", "", { "dependencies": { "@types/phoenix": "^1.6.6", "@types/ws": "^8.18.1", "tslib": "2.8.1", "ws": "^8.18.2" } }, "sha512-PWbnEMkcQRuor8jhObp4+Snufkq8C6fBp+MchVp2qBPY1NXk/c3Iv3YyiFYVzo0Dzuw4nAlT4+ahuPggy4r32w=="],
"@supabase/storage-js": ["@supabase/storage-js@2.90.1", "", { "dependencies": { "iceberg-js": "^0.8.1", "tslib": "2.8.1" } }, "sha512-GHY+Ps/K/RBfRj7kwx+iVf2HIdqOS43rM2iDOIDpapyUnGA9CCBFzFV/XvfzznGykd//z2dkGZhlZZprsVFqGg=="],
"@supabase/supabase-js": ["@supabase/supabase-js@2.90.1", "", { "dependencies": { "@supabase/auth-js": "2.90.1", "@supabase/functions-js": "2.90.1", "@supabase/postgrest-js": "2.90.1", "@supabase/realtime-js": "2.90.1", "@supabase/storage-js": "2.90.1" } }, "sha512-U8KaKGLUgTIFHtwEW1dgw1gK7XrdpvvYo7nzzqPx721GqPe8WZbAiLh/hmyKLGBYQ/mmQNr20vU9tWSDZpii3w=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.18", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.1", "lightningcss": "1.30.2", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.1.18" } }, "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ=="],
@@ -468,19 +436,19 @@
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.2", "", { "dependencies": { "@tanstack/query-devtools": "5.92.0" }, "peerDependencies": { "@tanstack/react-query": "^5.90.14", "react": "^18 || ^19" } }, "sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg=="], "@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.91.2", "", { "dependencies": { "@tanstack/query-devtools": "5.92.0" }, "peerDependencies": { "@tanstack/react-query": "^5.90.14", "react": "^18 || ^19" } }, "sha512-ZJ1503ay5fFeEYFUdo7LMNFzZryi6B0Cacrgr2h1JRkvikK1khgIq6Nq2EcblqEdIlgB/r7XDW8f8DQ89RuUgg=="],
"@tanstack/react-router": ["@tanstack/react-router@1.147.3", "", { "dependencies": { "@tanstack/history": "1.145.7", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.147.1", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-Fp9DoszYiIJclwxU43kyP/cqcWD418DPmV6yhmIOuVedsSMnfh2g7uRQ+bOoaWn996JjuU9yt/x48h66aCQSQA=="], "@tanstack/react-router": ["@tanstack/react-router@1.145.7", "", { "dependencies": { "@tanstack/history": "1.145.7", "@tanstack/react-store": "^0.8.0", "@tanstack/router-core": "1.145.7", "isbot": "^5.1.22", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-0O+a4TjJSPXd2BsvDPwDPBKRQKYqNIBg5TAg9NzCteqJ0NXRxwohyqCksHqCEEtJe/uItwqmHoqkK4q5MDhEsA=="],
"@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.149.0", "", { "dependencies": { "@tanstack/router-devtools-core": "1.149.0" }, "peerDependencies": { "@tanstack/react-router": "^1.147.3", "@tanstack/router-core": "^1.147.1", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-QJ6epMhRKTS8WrBmcMFjK1v+jDaimMQuySCSNA8NR1ZROKv3xx0gY8AjyVVgQ1h78HSXXRMYH3aql2kWYjc31g=="], "@tanstack/react-router-devtools": ["@tanstack/react-router-devtools@1.145.7", "", { "dependencies": { "@tanstack/router-devtools-core": "1.145.7" }, "peerDependencies": { "@tanstack/react-router": "^1.145.7", "@tanstack/router-core": "^1.145.7", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" }, "optionalPeers": ["@tanstack/router-core"] }, "sha512-crzHSQ/rcGX7RfuYsmm1XG5quurNMDTIApU7jfwDx5J9HnUxCOSJrbFX0L3w0o0VRCw5xhrL2EdCnW78Ic86hg=="],
"@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="], "@tanstack/react-store": ["@tanstack/react-store@0.8.0", "", { "dependencies": { "@tanstack/store": "0.8.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow=="],
"@tanstack/router-core": ["@tanstack/router-core@1.147.1", "", { "dependencies": { "@tanstack/history": "1.145.7", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.1", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-yf8o3CNgJVGO5JnIqiTe0y2eChxEM0w7TrEs1VSumL/zz2bQroYGNr1mOXJ2VeN+7YfJJwjEqq71P5CzWwMzRg=="], "@tanstack/router-core": ["@tanstack/router-core@1.145.7", "", { "dependencies": { "@tanstack/history": "1.145.7", "@tanstack/store": "^0.8.0", "cookie-es": "^2.0.0", "seroval": "^1.4.1", "seroval-plugins": "^1.4.0", "tiny-invariant": "^1.3.3", "tiny-warning": "^1.0.3" } }, "sha512-v6jx6JqVUBM0/FcBq1tX22xiPq8Ufc0PDEP582/4deYoq2/RYd+bZstANp3mGSsqdxE/luhoLYuuSQiwi/j1wA=="],
"@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.149.0", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.147.1", "csstype": "^3.0.10" }, "optionalPeers": ["csstype"] }, "sha512-dy9xb8U9VWAavqKM0sTFhAs2ufVs3d/cGSbqczIgBcAKCjjbsAng1gV4ezPXmfF1pa+2MW6n7SViXsxxvtCRiw=="], "@tanstack/router-devtools-core": ["@tanstack/router-devtools-core@1.145.7", "", { "dependencies": { "clsx": "^2.1.1", "goober": "^2.1.16", "tiny-invariant": "^1.3.3" }, "peerDependencies": { "@tanstack/router-core": "^1.145.7", "csstype": "^3.0.10", "solid-js": ">=1.9.5" }, "optionalPeers": ["csstype"] }, "sha512-oKeq/6QvN49THCh++FJyPv1X65i20qGS4aJHQTNsl4cu1piW1zWUhab2L3DZVr3G8C40FW3xb6hVw92N/fzZbQ=="],
"@tanstack/router-generator": ["@tanstack/router-generator@1.149.0", "", { "dependencies": { "@tanstack/router-core": "1.147.1", "@tanstack/router-utils": "1.143.11", "@tanstack/virtual-file-routes": "1.145.4", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-H+SZbJ9j4G+y/329LlRLb//4sBdPNQpuMddb/rgkfoRZpdztm9Ejm9EEbMJB0rkNDrSgfSPOZ6VtJbndYH/AQA=="], "@tanstack/router-generator": ["@tanstack/router-generator@1.145.7", "", { "dependencies": { "@tanstack/router-core": "1.145.7", "@tanstack/router-utils": "1.143.11", "@tanstack/virtual-file-routes": "1.145.4", "prettier": "^3.5.0", "recast": "^0.23.11", "source-map": "^0.7.4", "tsx": "^4.19.2", "zod": "^3.24.2" } }, "sha512-xg71c1WTku0ro0rgpJWh3Dt+ognV9qWe2KJHAPzrqfOYdUYu9sGq7Ri4jo8Rk0luXWZrWsrFdBP+9Jx6JH6zWA=="],
"@tanstack/router-plugin": ["@tanstack/router-plugin@1.149.0", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.147.1", "@tanstack/router-generator": "1.149.0", "@tanstack/router-utils": "1.143.11", "@tanstack/virtual-file-routes": "1.145.4", "babel-dead-code-elimination": "^1.0.11", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.147.3", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-DYPScneHZ0fm3FJyDhkUCW0w0dOopAKvep57n/Ft2b3RrHSeSeJB/cJWgsUvpcYJfpywkyOLyqVLMoiDvLoG/A=="], "@tanstack/router-plugin": ["@tanstack/router-plugin@1.145.7", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.145.7", "@tanstack/router-generator": "1.145.7", "@tanstack/router-utils": "1.143.11", "@tanstack/virtual-file-routes": "1.145.4", "babel-dead-code-elimination": "^1.0.11", "chokidar": "^3.6.0", "unplugin": "^2.1.2", "zod": "^3.24.2" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2", "@tanstack/react-router": "^1.145.7", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0", "vite-plugin-solid": "^2.11.10", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-Rimo0NragYKHwjoYX9JBLS8VkZD4D/LqzzLIlX9yz93lmWFRu/DbuS7fDZNqX1Ea8naNvo18DlySszYLzC8XDg=="],
"@tanstack/router-utils": ["@tanstack/router-utils@1.143.11", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-N24G4LpfyK8dOlnP8BvNdkuxg1xQljkyl6PcrdiPSA301pOjatRT1y8wuCCJZKVVD8gkd0MpCZ0VEjRMGILOtA=="], "@tanstack/router-utils": ["@tanstack/router-utils@1.143.11", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "ansis": "^4.1.0", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-N24G4LpfyK8dOlnP8BvNdkuxg1xQljkyl6PcrdiPSA301pOjatRT1y8wuCCJZKVVD8gkd0MpCZ0VEjRMGILOtA=="],
@@ -504,8 +472,6 @@
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
@@ -516,35 +482,31 @@
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
"@types/node": ["@types/node@22.19.5", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-HfF8+mYcHPcPypui3w3mvzuIErlNOh2OAG+BCeBZCEwyiD5ls2SiCwEyT47OELtf7M3nHxBdu0FsmzdKxkN52Q=="], "@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="],
"@types/phoenix": ["@types/phoenix@1.6.7", "", {}, "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q=="], "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="],
"@types/react": ["@types/react@19.2.8", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-3MbSL37jEchWZz2p2mjntRZtPt837ij10ApxKfgmXCTuHWagYg7iA5bqPw6C8BMPfwidlvfPI/fxOc42HLhcyg=="],
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.52.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/type-utils": "8.52.0", "@typescript-eslint/utils": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.52.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-okqtOgqu2qmZJ5iN4TWlgfF171dZmx2FzdOv2K/ixL2LZWDStL8+JgQerI2sa8eAEfoydG9+0V96m7V+P8yE1Q=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.53.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/type-utils": "8.53.0", "@typescript-eslint/utils": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.53.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-eEXsVvLPu8Z4PkFibtuFJLJOTAV/nPdgtSjkGoPpddpFk3/ym2oy97jynY6ic2m6+nc5M8SE1e9v/mHKsulcJg=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.52.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-iIACsx8pxRnguSYhHiMn2PvhvfpopO9FXHyn1mG5txZIsAaB6F0KwbFnUQN3KCiG3Jcuad/Cao2FAs1Wp7vAyg=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.53.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg=="], "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.52.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.52.0", "@typescript-eslint/types": "^8.52.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xD0MfdSdEmeFa3OmVqonHi+Cciab96ls1UhIF/qX/O/gPu5KXD0bY9lu33jj04fjzrXHcuvjBcBC+D3SNSadaw=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.53.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.53.0", "@typescript-eslint/types": "^8.53.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Bl6Gdr7NqkqIP5yP9z1JU///Nmes4Eose6L1HwpuVHwScgDPPuEWbUVhvlZmb8hy0vX9syLk5EGNL700WcBlbg=="], "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0" } }, "sha512-ixxqmmCcc1Nf8S0mS0TkJ/3LKcC8mruYJPOU6Ia2F/zUUR4pApW7LzrpU3JmtePbRUTes9bEqRc1Gg4iyRnDzA=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0" } }, "sha512-kWNj3l01eOGSdVBnfAF2K1BTh06WS0Yet6JUgb9Cmkqaz3Jlu0fdVUjj9UI8gPidBWSMqDIglmEXifSgDT/D0g=="], "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.52.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-jl+8fzr/SdzdxWJznq5nvoI7qn2tNYV/ZBAEcaFMVXf+K6jmXvAFrgo/+5rxgnL152f//pDEAYAhhBAZGrVfwg=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.53.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-K6Sc0R5GIG6dNoPdOooQ+KtvT5KCKAvTcY8h2rIuul19vxH5OTQk7ArKkd4yTzkw66WnNY0kPPzzcmWA+XRmiA=="], "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/utils": "8.52.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-JD3wKBRWglYRQkAtsyGz1AewDu3mTc7NtRjR/ceTyGoPqmdS5oCdx/oZMWD5Zuqmo6/MpsYs0wp6axNt88/2EQ=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/utils": "8.53.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-BBAUhlx7g4SmcLhn8cnbxoxtmS7hcq39xKCgiutL3oNx1TaIp+cny51s8ewnKMpVUKQUGb41RAUWZ9kxYdovuw=="], "@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.53.0", "", {}, "sha512-Bmh9KX31Vlxa13+PqPvt4RzKRN1XORYSLlAE+sO1i28NkisGbTtSLFVB3l7PWdHtR3E0mVMuC7JilWJ99m2HxQ=="], "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.52.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.52.0", "@typescript-eslint/tsconfig-utils": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/visitor-keys": "8.52.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XP3LClsCc0FsTK5/frGjolyADTh3QmsLp6nKd476xNI9CsSsLnmn4f0jrzNoAulmxlmNIpeXuHYeEQv61Q6qeQ=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.53.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.53.0", "@typescript-eslint/tsconfig-utils": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/visitor-keys": "8.53.0", "debug": "^4.4.3", "minimatch": "^9.0.5", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pw0c0Gdo7Z4xOG987u3nJ8akL9093yEEKv8QTJ+Bhkghj1xyj8cgPaavlr9rq8h7+s6plUJ4QJYw2gCZodqmGw=="], "@typescript-eslint/utils": ["@typescript-eslint/utils@8.52.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.52.0", "@typescript-eslint/types": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-wYndVMWkweqHpEpwPhwqE2lnD2DxC6WVLupU/DOt/0/v+/+iQbbzO3jOHjmBMnhu0DgLULvOaU4h4pwHYi2oRQ=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.53.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XDY4mXTez3Z1iRDI5mbRhH4DFSt46oaIFsLg+Zn97+sYrXACziXSQcSelMybnVZ5pa1P6xYkPr5cMJyunM1ZDA=="], "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.53.0", "", { "dependencies": { "@typescript-eslint/types": "8.53.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-LZ2NqIHFhvFwxG0qZeLL9DvdNAHPGCY5dIRwBhyYeU+LfLhcStE1ImjsuTG/WaVh3XysGaeLW8Rqq7cGkPCFvw=="],
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="], "@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
@@ -644,7 +606,7 @@
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"axe-core": ["axe-core@4.11.1", "", {}, "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A=="], "axe-core": ["axe-core@4.11.0", "", {}, "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
@@ -652,12 +614,10 @@
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.9.14", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="],
"bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
"bin-links": ["bin-links@6.0.0", "", { "dependencies": { "cmd-shim": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "proc-log": "^6.0.0", "read-cmd-shim": "^6.0.0", "write-file-atomic": "^7.0.0" } }, "sha512-X4CiKlcV2GjnCMwnKAfbVWpHa++65th9TuzAEYtZoATiOE2DQKhSp4CJlyLoTqdhBKlXjpXjCTYPNNFS33Fi6w=="],
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
@@ -666,8 +626,6 @@
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="], "browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
@@ -678,7 +636,7 @@
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001764", "", {}, "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g=="], "caniuse-lite": ["caniuse-lite@1.0.30001762", "", {}, "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw=="],
"chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="],
@@ -688,14 +646,10 @@
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"cmd-shim": ["cmd-shim@8.0.0", "", {}, "sha512-Jk/BK6NCapZ58BKUxlSI+ouKRbjH1NLZCgJkYoab+vEHUY3f6OzpNBN9u7HFSv9J6TRDGs4PLOHezoKGaFRSCA=="],
"cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
@@ -714,14 +668,12 @@
"css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="], "css-tree": ["css-tree@3.1.0", "", { "dependencies": { "mdn-data": "2.12.2", "source-map-js": "^1.0.1" } }, "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w=="],
"cssstyle": ["cssstyle@5.3.7", "", { "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", "css-tree": "^3.1.0", "lru-cache": "^11.2.4" } }, "sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ=="], "cssstyle": ["cssstyle@5.3.6", "", { "dependencies": { "@asamuzakjp/css-color": "^4.1.1", "@csstools/css-syntax-patches-for-csstree": "^1.0.21", "css-tree": "^3.1.0", "lru-cache": "^11.2.4" } }, "sha512-legscpSpgSAeGEe0TNcai97DKt9Vd9AsAdOL7Uoetb52Ar/8eJm3LIa39qpv8wWzLFlNG4vVvppQM+teaMPj3A=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="], "damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA=="],
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
"data-urls": ["data-urls@6.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.0.0" } }, "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA=="], "data-urls": ["data-urls@6.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^15.0.0" } }, "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA=="],
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
@@ -730,8 +682,6 @@
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
"date-fns": ["date-fns@4.1.0", "", {}, "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
@@ -750,7 +700,7 @@
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], "doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
@@ -818,9 +768,9 @@
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
"eslint-visitor-keys": ["eslint-visitor-keys@5.0.0", "", {}, "sha512-A0XeIi7CXU7nPlfHS9loMYEKxUaONu/hTEzHTGba9Huu94Cq1hPivf+DE5erJozZOky0LfvXAyrV/tcswpLI0Q=="], "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"espree": ["espree@11.0.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-+gMeWRrIh/NsG+3NaLeWHuyeyk70p2tbvZIWBYcqQ4/7Xvars6GYTZNhF1sIeLcc6Wb11He5ffz3hsHyXFrw5A=="], "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
@@ -844,8 +794,6 @@
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
@@ -858,9 +806,7 @@
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], "framer-motion": ["framer-motion@12.24.7", "", { "dependencies": { "motion-dom": "^12.24.3", "motion-utils": "^12.23.28", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-EolFLm7NdEMhWO/VTMZ0LlR4fLHGDiJItTx3i8dlyQooOOBoYAaysK4paGD4PrwqnoDdeDOS+TxnSBIAnNHs3w=="],
"framer-motion": ["framer-motion@12.26.1", "", { "dependencies": { "motion-dom": "^12.24.11", "motion-utils": "^12.24.10", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Uzc8wGldU4FpmGotthjjcj0SZhigcODjqvKT7lzVZHsmYkzQMFfMIv0vHQoXCeoe/Ahxqp4by4A6QbzFA/lblw=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
@@ -916,14 +862,12 @@
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="], "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
"iceberg-js": ["iceberg-js@0.8.1", "", {}, "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
@@ -1070,15 +1014,11 @@
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], "motion": ["motion@12.24.7", "", { "dependencies": { "framer-motion": "^12.24.7", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-0jOoqFlQ7JBvAcRhRv28pwUgZ1xw9WS4+tCU6aqYjxgiNVZCVi34ED2cihW3EgjIIWPBoZJis5og1mx/LmQWVQ=="],
"minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], "motion-dom": ["motion-dom@12.24.3", "", { "dependencies": { "motion-utils": "^12.23.28" } }, "sha512-ZjMZCwhTglim0LM64kC1iFdm4o+2P9IKk3rl/Nb4RKsb5p4O9HJ1C2LWZXOFdsRtp6twpqWRXaFKOduF30ntow=="],
"motion": ["motion@12.26.1", "", { "dependencies": { "framer-motion": "^12.26.1", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-IVhzx9HOQTiJ9ykthMOlZPnLwrkXziN5Q/yebsqBYlFJb2rHP8yhmKc8O/YUT9byPJlxOeqkzfNYCrVKZx8vqg=="], "motion-utils": ["motion-utils@12.23.28", "", {}, "sha512-0W6cWd5Okoyf8jmessVK3spOmbyE0yTdNKujHctHH9XdAE4QDuZ1/LjSXC68rrhsJU+TkzXURC5OdSWh9ibOwQ=="],
"motion-dom": ["motion-dom@12.24.11", "", { "dependencies": { "motion-utils": "^12.24.10" } }, "sha512-DlWOmsXMJrV8lzZyd+LKjG2CXULUs++bkq8GZ2Sr0R0RRhs30K2wtY+LKiTjhmJU3W61HK+rB0GLz6XmPvTA1A=="],
"motion-utils": ["motion-utils@12.24.10", "", {}, "sha512-x5TFgkCIP4pPsRLpKoI86jv/q8t8FQOiM/0E8QKBzfMozWHfkKap2gA1hOki+B5g3IsBNpxbUnfOum1+dgvYww=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
@@ -1088,16 +1028,10 @@
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
"node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="],
"node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="],
"node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], "node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"npm-normalize-package-bin": ["npm-normalize-package-bin@5.0.0", "", {}, "sha512-CJi3OS4JLsNMmr2u07OJlhcrPxCeOeP/4xq67aWNai6TNWWbTrlNDgl8NcFKVlcBKp18GPj+EzbNIgrBfZhsag=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
@@ -1148,8 +1082,6 @@
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="], "pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
"proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="],
@@ -1166,8 +1098,6 @@
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
"read-cmd-shim": ["read-cmd-shim@6.0.0", "", {}, "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A=="],
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
@@ -1224,8 +1154,6 @@
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
"solid-js": ["solid-js@1.9.10", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew=="], "solid-js": ["solid-js@1.9.10", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.3.0", "seroval-plugins": "~1.3.0" } }, "sha512-Coz956cos/EPDlhs6+jsdTxKuJDPT7B5SVIWgABwROyxjY7Xbr8wkzD68Et+NxnV7DLJ3nJdAC2r9InuV/4Jew=="],
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
@@ -1254,8 +1182,6 @@
"strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
"supabase": ["supabase@2.72.4", "", { "dependencies": { "bin-links": "^6.0.0", "https-proxy-agent": "^7.0.2", "node-fetch": "^3.3.2", "tar": "7.5.2" }, "bin": { "supabase": "bin/supabase" } }, "sha512-7jbpr9svviXihYhUqBK7k7U3aRo4x8OVSkzxrl+cpH4svDS5+Rl605DW9ijYFeRuNQZEvBkRaJQ93bGORWNFqQ=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
@@ -1268,8 +1194,6 @@
"tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="],
"tar": ["tar@7.5.2", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg=="],
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
"tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="], "tiny-warning": ["tiny-warning@1.0.3", "", {}, "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="],
@@ -1320,7 +1244,7 @@
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"typescript-eslint": ["typescript-eslint@8.53.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.53.0", "@typescript-eslint/parser": "8.53.0", "@typescript-eslint/typescript-estree": "8.53.0", "@typescript-eslint/utils": "8.53.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-xHURCQNxZ1dsWn0sdOaOfCSQG0HKeqSj9OexIxrz6ypU6wHYOdX2I3D2b8s8wFSsSOYJb+6q283cLiLlkEsBYw=="], "typescript-eslint": ["typescript-eslint@8.52.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.52.0", "@typescript-eslint/parser": "8.52.0", "@typescript-eslint/typescript-estree": "8.52.0", "@typescript-eslint/utils": "8.52.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-atlQQJ2YkO4pfTVQmQ+wvYQwexPDOIgo+RaVcD7gHgzy/IQA+XTyuxNM9M9TVXvttkF7koBHmcwisKdOAf2EcA=="],
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
@@ -1336,13 +1260,11 @@
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
"use-debounce": ["use-debounce@10.1.0", "", { "peerDependencies": { "react": "*" } }, "sha512-lu87Za35V3n/MyMoEpD5zJv0k7hCn0p+V/fK2kWD+3k2u3kOCwO593UArbczg1fhfs2rqPEnHpULJ3KmGdDzvg=="],
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
"use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
"vite": ["vite@7.3.1", "", { "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" }, "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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], "vite": ["vite@7.3.0", "", { "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" }, "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" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg=="],
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
@@ -1352,8 +1274,6 @@
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="], "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"web-vitals": ["web-vitals@5.1.0", "", {}, "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg=="], "web-vitals": ["web-vitals@5.1.0", "", {}, "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg=="],
"webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="], "webidl-conversions": ["webidl-conversions@8.0.1", "", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="],
@@ -1378,15 +1298,13 @@
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
"write-file-atomic": ["write-file-atomic@7.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^4.0.1" } }, "sha512-YnlPC6JqnZl6aO4uRc+dx5PHguiR9S6WeoLtpxNT9wIG+BDya7ZNE1q7KOjVgaA73hKhKLpVPgJ5QA9THQ5BRg=="],
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
"xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
"xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="], "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
@@ -1398,34 +1316,64 @@
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
"@eslint/eslintrc/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
"@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-arrow/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-avatar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="], "@radix-ui/react-checkbox/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-avatar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], "@radix-ui/react-checkbox/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-collection/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-collection/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-dialog/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], "@radix-ui/react-dismissable-layer/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-focus-scope/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-popover/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-popover/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-popper/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-popper/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-roving-focus/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-roving-focus/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-select/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-select/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="], "@radix-ui/react-tabs/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-tabs/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-tooltip/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.8.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.8.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
@@ -1446,20 +1394,12 @@
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
"eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"eslint/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"eslint/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
"eslint-compat-utils/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "eslint-compat-utils/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
@@ -1492,15 +1432,25 @@
"tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], "tsconfig-paths/json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
"vue-eslint-parser/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
"vue-eslint-parser/espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
"vue-eslint-parser/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "vue-eslint-parser/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "@radix-ui/react-arrow/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@eslint/eslintrc/espree/eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], "@radix-ui/react-checkbox/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-focus-scope/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-popper/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@tanstack/devtools/@tanstack/devtools-client/@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.3.5", "", {}, "sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw=="], "@tanstack/devtools/@tanstack/devtools-client/@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.3.5", "", {}, "sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw=="],

View File

@@ -1,14 +1,44 @@
import { useState } from 'react'; import { useEffect, useState } from 'react'
import { Plus, Search, BookOpen, Trash2, Library, Edit3, Save } from 'lucide-react'; import {
import { Card, CardContent } from '@/components/ui/card'; Plus,
import { Button } from '@/components/ui/button'; Search,
import { Input } from '@/components/ui/input'; BookOpen,
import { Textarea } from '@/components/ui/textarea'; Trash2,
import { Badge } from '@/components/ui/badge'; Library,
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'; Edit3,
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; Save,
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog'; } from 'lucide-react'
import { cn } from '@/lib/utils'; import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Textarea } from '@/components/ui/textarea'
import { Badge } from '@/components/ui/badge'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog'
import { cn } from '@/lib/utils'
import { useSubjectBibliografia } from '@/data/hooks/useSubjects'
//import { toast } from 'sonner'; //import { toast } from 'sonner';
//import { mockLibraryResources } from '@/data/mockMateriaData'; //import { mockLibraryResources } from '@/data/mockMateriaData';
@@ -20,7 +50,7 @@ export const mockLibraryResources = [
editorial: 'MIT Press', editorial: 'MIT Press',
anio: 2016, anio: 2016,
isbn: '9780262035613', isbn: '9780262035613',
disponible: true disponible: true,
}, },
{ {
id: 'lib-2', id: 'lib-2',
@@ -29,102 +59,154 @@ export const mockLibraryResources = [
editorial: 'Pearson', editorial: 'Pearson',
anio: 2020, anio: 2020,
isbn: '9780134610993', isbn: '9780134610993',
disponible: true disponible: true,
}, },
{ {
id: 'lib-3', id: 'lib-3',
titulo: 'Hands-On Machine Learning', titulo: 'Hands-On Machine Learning',
autor: 'Aurélien Géron', autor: 'Aurélien Géron',
editorial: 'O\'Reilly Media', editorial: "O'Reilly Media",
anio: 2019, anio: 2019,
isbn: '9781492032649', isbn: '9781492032649',
disponible: false disponible: false,
} },
]; ]
// --- Interfaces --- // --- Interfaces ---
export interface BibliografiaEntry { export interface BibliografiaEntry {
id: string; id: string
tipo: 'BASICA' | 'COMPLEMENTARIA'; tipo: 'BASICA' | 'COMPLEMENTARIA'
cita: string; cita: string
fuenteBibliotecaId?: string; tipo_fuente?: 'MANUAL' | 'BIBLIOTECA'
fuenteBiblioteca?: any; biblioteca_item_id?: string | null
fuenteBibliotecaId?: string
fuenteBiblioteca?: any
} }
interface BibliografiaTabProps { interface BibliografiaTabProps {
bibliografia: BibliografiaEntry[]; bibliografia: BibliografiaEntry[]
onSave: (bibliografia: BibliografiaEntry[]) => void; onSave: (bibliografia: BibliografiaEntry[]) => void
isSaving: boolean; isSaving: boolean
} }
export function BibliographyItem({ bibliografia, onSave, isSaving }: BibliografiaTabProps) { export function BibliographyItem({
const [entries, setEntries] = useState<BibliografiaEntry[]>(bibliografia); bibliografia,
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false); onSave,
const [isLibraryDialogOpen, setIsLibraryDialogOpen] = useState(false); isSaving,
const [deleteId, setDeleteId] = useState<string | null>(null); }: BibliografiaTabProps) {
const [editingId, setEditingId] = useState<string | null>(null); const { data: bibliografia2, isLoading: loadinmateria } =
const [newEntryType, setNewEntryType] = useState<'BASICA' | 'COMPLEMENTARIA'>('BASICA'); useSubjectBibliografia('9d4dda6a-488f-428a-8a07-38081592a641')
const [entries, setEntries] = useState<BibliografiaEntry[]>(bibliografia)
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false)
const [isLibraryDialogOpen, setIsLibraryDialogOpen] = useState(false)
const [deleteId, setDeleteId] = useState<string | null>(null)
const [editingId, setEditingId] = useState<string | null>(null)
const [newEntryType, setNewEntryType] = useState<'BASICA' | 'COMPLEMENTARIA'>(
'BASICA',
)
const basicaEntries = entries.filter(e => e.tipo === 'BASICA'); useEffect(() => {
const complementariaEntries = entries.filter(e => e.tipo === 'COMPLEMENTARIA'); if (bibliografia2 && Array.isArray(bibliografia2)) {
setEntries(bibliografia2)
} else if (bibliografia) {
// Fallback a la prop inicial si la API no devuelve nada
setEntries(bibliografia)
}
}, [bibliografia2, bibliografia])
const basicaEntries = entries.filter((e) => e.tipo === 'BASICA')
const complementariaEntries = entries.filter(
(e) => e.tipo === 'COMPLEMENTARIA',
)
console.log(bibliografia2)
const handleAddManual = (cita: string) => { const handleAddManual = (cita: string) => {
const newEntry: BibliografiaEntry = { id: `manual-${Date.now()}`, tipo: newEntryType, cita }; const newEntry: BibliografiaEntry = {
setEntries([...entries, newEntry]); id: `manual-${Date.now()}`,
setIsAddDialogOpen(false); tipo: newEntryType,
cita,
}
setEntries([...entries, newEntry])
setIsAddDialogOpen(false)
//toast.success('Referencia manual añadida'); //toast.success('Referencia manual añadida');
}; }
const handleAddFromLibrary = (resource: any, tipo: 'BASICA' | 'COMPLEMENTARIA') => { const handleAddFromLibrary = (
const cita = `${resource.autor} (${resource.anio}). ${resource.titulo}. ${resource.editorial}.`; resource: any,
tipo: 'BASICA' | 'COMPLEMENTARIA',
) => {
const cita = `${resource.autor} (${resource.anio}). ${resource.titulo}. ${resource.editorial}.`
const newEntry: BibliografiaEntry = { const newEntry: BibliografiaEntry = {
id: `lib-ref-${Date.now()}`, id: `lib-ref-${Date.now()}`,
tipo, tipo,
cita, cita,
fuenteBibliotecaId: resource.id, fuenteBibliotecaId: resource.id,
fuenteBiblioteca: resource, fuenteBiblioteca: resource,
}; }
setEntries([...entries, newEntry]); setEntries([...entries, newEntry])
setIsLibraryDialogOpen(false); setIsLibraryDialogOpen(false)
//toast.success('Añadido desde biblioteca'); //toast.success('Añadido desde biblioteca');
}; }
const handleUpdateCita = (id: string, cita: string) => { const handleUpdateCita = (id: string, cita: string) => {
setEntries(entries.map(e => e.id === id ? { ...e, cita } : e)); setEntries(entries.map((e) => (e.id === id ? { ...e, cita } : e)))
}; }
return ( return (
<div className="max-w-5xl mx-auto py-10 space-y-8 animate-in fade-in duration-500"> <div className="animate-in fade-in mx-auto max-w-5xl space-y-8 py-10 duration-500">
<div className="flex items-center justify-between border-b pb-4"> <div className="flex items-center justify-between border-b pb-4">
<div> <div>
<h2 className="text-2xl font-bold text-slate-900 tracking-tight">Bibliografía</h2> <h2 className="text-2xl font-bold tracking-tight text-slate-900">
<p className="text-sm text-slate-500 mt-1"> Bibliografía
{basicaEntries.length} básica {complementariaEntries.length} complementaria </h2>
<p className="mt-1 text-sm text-slate-500">
{basicaEntries.length} básica {complementariaEntries.length}{' '}
complementaria
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Dialog open={isLibraryDialogOpen} onOpenChange={setIsLibraryDialogOpen}> <Dialog
open={isLibraryDialogOpen}
onOpenChange={setIsLibraryDialogOpen}
>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline" className="border-blue-200 text-blue-700 hover:bg-blue-50"> <Button
<Library className="w-4 h-4 mr-2" /> Buscar en biblioteca variant="outline"
className="border-blue-200 text-blue-700 hover:bg-blue-50"
>
<Library className="mr-2 h-4 w-4" /> Buscar en biblioteca
</Button> </Button>
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-2xl"> <DialogContent className="max-w-2xl">
<LibrarySearchDialog onSelect={handleAddFromLibrary} existingIds={entries.map(e => e.fuenteBibliotecaId || '')} /> <LibrarySearchDialog
onSelect={handleAddFromLibrary}
existingIds={entries.map((e) => e.fuenteBibliotecaId || '')}
/>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}> <Dialog open={isAddDialogOpen} onOpenChange={setIsAddDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button variant="outline"><Plus className="w-4 h-4 mr-2" /> Añadir manual</Button> <Button variant="outline">
<Plus className="mr-2 h-4 w-4" /> Añadir manual
</Button>
</DialogTrigger> </DialogTrigger>
<DialogContent> <DialogContent>
<AddManualDialog tipo={newEntryType} onTypeChange={setNewEntryType} onAdd={handleAddManual} /> <AddManualDialog
tipo={newEntryType}
onTypeChange={setNewEntryType}
onAdd={handleAddManual}
/>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Button onClick={() => onSave(entries)} disabled={isSaving} className="bg-blue-600 hover:bg-blue-700"> <Button
<Save className="w-4 h-4 mr-2" /> {isSaving ? 'Guardando...' : 'Guardar'} onClick={() => onSave(entries)}
disabled={isSaving}
className="bg-blue-600 hover:bg-blue-700"
>
<Save className="mr-2 h-4 w-4" />{' '}
{isSaving ? 'Guardando...' : 'Guardar'}
</Button> </Button>
</div> </div>
</div> </div>
@@ -133,14 +215,16 @@ export function BibliographyItem({ bibliografia, onSave, isSaving }: Bibliografi
{/* BASICA */} {/* BASICA */}
<section className="space-y-4"> <section className="space-y-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-4 w-1 bg-blue-600 rounded-full" /> <div className="h-4 w-1 rounded-full bg-blue-600" />
<h3 className="font-semibold text-slate-800">Bibliografía Básica</h3> <h3 className="font-semibold text-slate-800">
Bibliografía Básica
</h3>
</div> </div>
<div className="grid gap-3"> <div className="grid gap-3">
{basicaEntries.map(entry => ( {basicaEntries.map((entry) => (
<BibliografiaCard <BibliografiaCard
key={entry.id} key={entry.id}
entry={entry} entry={entry}
isEditing={editingId === entry.id} isEditing={editingId === entry.id}
onEdit={() => setEditingId(entry.id)} onEdit={() => setEditingId(entry.id)}
onStopEditing={() => setEditingId(null)} onStopEditing={() => setEditingId(null)}
@@ -154,14 +238,16 @@ export function BibliographyItem({ bibliografia, onSave, isSaving }: Bibliografi
{/* COMPLEMENTARIA */} {/* COMPLEMENTARIA */}
<section className="space-y-4"> <section className="space-y-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="h-4 w-1 bg-slate-400 rounded-full" /> <div className="h-4 w-1 rounded-full bg-slate-400" />
<h3 className="font-semibold text-slate-800">Bibliografía Complementaria</h3> <h3 className="font-semibold text-slate-800">
Bibliografía Complementaria
</h3>
</div> </div>
<div className="grid gap-3"> <div className="grid gap-3">
{complementariaEntries.map(entry => ( {complementariaEntries.map((entry) => (
<BibliografiaCard <BibliografiaCard
key={entry.id} key={entry.id}
entry={entry} entry={entry}
isEditing={editingId === entry.id} isEditing={editingId === entry.id}
onEdit={() => setEditingId(entry.id)} onEdit={() => setEditingId(entry.id)}
onStopEditing={() => setEditingId(null)} onStopEditing={() => setEditingId(null)}
@@ -177,70 +263,143 @@ export function BibliographyItem({ bibliografia, onSave, isSaving }: Bibliografi
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>¿Eliminar referencia?</AlertDialogTitle> <AlertDialogTitle>¿Eliminar referencia?</AlertDialogTitle>
<AlertDialogDescription>La referencia será quitada del plan de estudios.</AlertDialogDescription> <AlertDialogDescription>
La referencia será quitada del plan de estudios.
</AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel> <AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction onClick={() => { setEntries(entries.filter(e => e.id !== deleteId)); setDeleteId(null); }} className="bg-red-600">Eliminar</AlertDialogAction> <AlertDialogAction
onClick={() => {
setEntries(entries.filter((e) => e.id !== deleteId))
setDeleteId(null)
}}
className="bg-red-600"
>
Eliminar
</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
</div> </div>
); )
} }
// --- Subcomponentes --- // --- Subcomponentes ---
function BibliografiaCard({ entry, isEditing, onEdit, onStopEditing, onUpdateCita, onDelete }: any) { function BibliografiaCard({
const [localCita, setLocalCita] = useState(entry.cita); entry,
isEditing,
onEdit,
onStopEditing,
onUpdateCita,
onDelete,
}: any) {
const [localCita, setLocalCita] = useState(entry.cita)
return ( return (
<Card className={cn("group transition-all hover:shadow-md", isEditing && "ring-2 ring-blue-500")}> <Card
className={cn(
'group transition-all hover:shadow-md',
isEditing && 'ring-2 ring-blue-500',
)}
>
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-start gap-4"> <div className="flex items-start gap-4">
<BookOpen className={cn("w-5 h-5 mt-1", entry.tipo === 'BASICA' ? "text-blue-600" : "text-slate-400")} /> <BookOpen
<div className="flex-1 min-w-0"> className={cn(
'mt-1 h-5 w-5',
entry.tipo === 'BASICA' ? 'text-blue-600' : 'text-slate-400',
)}
/>
<div className="min-w-0 flex-1">
{isEditing ? ( {isEditing ? (
<div className="space-y-2"> <div className="space-y-2">
<Textarea value={localCita} onChange={(e) => setLocalCita(e.target.value)} className="min-h-[80px]" /> <Textarea
value={localCita}
onChange={(e) => setLocalCita(e.target.value)}
className="min-h-[80px]"
/>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button variant="ghost" size="sm" onClick={onStopEditing}>Cancelar</Button> <Button variant="ghost" size="sm" onClick={onStopEditing}>
<Button size="sm" className="bg-emerald-600" onClick={() => { onUpdateCita(entry.id, localCita); onStopEditing(); }}>Guardar</Button> Cancelar
</Button>
<Button
size="sm"
className="bg-emerald-600"
onClick={() => {
onUpdateCita(entry.id, localCita)
onStopEditing()
}}
>
Guardar
</Button>
</div> </div>
</div> </div>
) : ( ) : (
<div onClick={onEdit} className="cursor-pointer"> <div onClick={onEdit} className="cursor-pointer">
<p className="text-sm leading-relaxed text-slate-700">{entry.cita}</p> <p className="text-sm leading-relaxed text-slate-700">
{entry.cita}
</p>
{entry.fuenteBiblioteca && ( {entry.fuenteBiblioteca && (
<div className="flex gap-2 mt-2"> <div className="mt-2 flex gap-2">
<Badge variant="secondary" className="text-[10px] bg-slate-100 text-slate-600">Biblioteca</Badge> <Badge
{entry.fuenteBiblioteca.disponible && <Badge className="text-[10px] bg-emerald-50 text-emerald-700 border-emerald-100">Disponible</Badge>} variant="secondary"
className="bg-slate-100 text-[10px] text-slate-600"
>
Biblioteca
</Badge>
{entry.fuenteBiblioteca.disponible && (
<Badge className="border-emerald-100 bg-emerald-50 text-[10px] text-emerald-700">
Disponible
</Badge>
)}
</div> </div>
)} )}
</div> </div>
)} )}
</div> </div>
{!isEditing && ( {!isEditing && (
<div className="flex opacity-0 group-hover:opacity-100 transition-opacity"> <div className="flex opacity-0 transition-opacity group-hover:opacity-100">
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-blue-600" onClick={onEdit}><Edit3 className="w-4 h-4" /></Button> <Button
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-red-500" onClick={onDelete}><Trash2 className="w-4 h-4" /></Button> variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-blue-600"
onClick={onEdit}
>
<Edit3 className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-red-500"
onClick={onDelete}
>
<Trash2 className="h-4 w-4" />
</Button>
</div> </div>
)} )}
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
); )
} }
function AddManualDialog({ tipo, onTypeChange, onAdd }: any) { function AddManualDialog({ tipo, onTypeChange, onAdd }: any) {
const [cita, setCita] = useState(''); const [cita, setCita] = useState('')
return ( return (
<div className="space-y-4 py-4"> <div className="space-y-4 py-4">
<DialogHeader><DialogTitle>Referencia Manual</DialogTitle></DialogHeader> <DialogHeader>
<DialogTitle>Referencia Manual</DialogTitle>
</DialogHeader>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-bold uppercase text-slate-500">Tipo</label> <label className="text-xs font-bold text-slate-500 uppercase">
Tipo
</label>
<Select value={tipo} onValueChange={onTypeChange}> <Select value={tipo} onValueChange={onTypeChange}>
<SelectTrigger><SelectValue /></SelectTrigger> <SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="BASICA">Básica</SelectItem> <SelectItem value="BASICA">Básica</SelectItem>
<SelectItem value="COMPLEMENTARIA">Complementaria</SelectItem> <SelectItem value="COMPLEMENTARIA">Complementaria</SelectItem>
@@ -248,44 +407,78 @@ function AddManualDialog({ tipo, onTypeChange, onAdd }: any) {
</Select> </Select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-xs font-bold uppercase text-slate-500">Cita APA</label> <label className="text-xs font-bold text-slate-500 uppercase">
<Textarea value={cita} onChange={(e) => setCita(e.target.value)} placeholder="Autor, A. (Año). Título..." className="min-h-[120px]" /> Cita APA
</label>
<Textarea
value={cita}
onChange={(e) => setCita(e.target.value)}
placeholder="Autor, A. (Año). Título..."
className="min-h-[120px]"
/>
</div> </div>
<Button onClick={() => onAdd(cita)} disabled={!cita.trim()} className="w-full bg-blue-600">Añadir a la lista</Button> <Button
onClick={() => onAdd(cita)}
disabled={!cita.trim()}
className="w-full bg-blue-600"
>
Añadir a la lista
</Button>
</div> </div>
); )
} }
function LibrarySearchDialog({ onSelect, existingIds }: any) { function LibrarySearchDialog({ onSelect, existingIds }: any) {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('')
const [tipo, setTipo] = useState<'BASICA' | 'COMPLEMENTARIA'>('BASICA'); const [tipo, setTipo] = useState<'BASICA' | 'COMPLEMENTARIA'>('BASICA')
const filtered = mockLibraryResources.filter(r => const filtered = mockLibraryResources.filter(
!existingIds.includes(r.id) && r.titulo.toLowerCase().includes(search.toLowerCase()) (r) =>
); !existingIds.includes(r.id) &&
r.titulo.toLowerCase().includes(search.toLowerCase()),
)
return ( return (
<div className="space-y-4 py-2"> <div className="space-y-4 py-2">
<DialogHeader><DialogTitle>Catálogo de Biblioteca</DialogTitle></DialogHeader> <DialogHeader>
<DialogTitle>Catálogo de Biblioteca</DialogTitle>
</DialogHeader>
<div className="flex gap-2"> <div className="flex gap-2">
<div className="relative flex-1"> <div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" /> <Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-slate-400" />
<Input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="Buscar por título o autor..." className="pl-10" /> <Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Buscar por título o autor..."
className="pl-10"
/>
</div> </div>
<Select value={tipo} onValueChange={(v:any) => setTipo(v)}><SelectTrigger className="w-36"><SelectValue /></SelectTrigger> <Select value={tipo} onValueChange={(v: any) => setTipo(v)}>
<SelectContent><SelectItem value="BASICA">Básica</SelectItem><SelectItem value="COMPLEMENTARIA">Complem.</SelectItem></SelectContent> <SelectTrigger className="w-36">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="BASICA">Básica</SelectItem>
<SelectItem value="COMPLEMENTARIA">Complem.</SelectItem>
</SelectContent>
</Select> </Select>
</div> </div>
<div className="max-h-[300px] overflow-y-auto pr-2 space-y-2"> <div className="max-h-[300px] space-y-2 overflow-y-auto pr-2">
{filtered.map(res => ( {filtered.map((res) => (
<div key={res.id} onClick={() => onSelect(res, tipo)} className="p-3 border rounded-lg hover:bg-slate-50 cursor-pointer flex justify-between items-center group"> <div
key={res.id}
onClick={() => onSelect(res, tipo)}
className="group flex cursor-pointer items-center justify-between rounded-lg border p-3 hover:bg-slate-50"
>
<div> <div>
<p className="text-sm font-semibold text-slate-700">{res.titulo}</p> <p className="text-sm font-semibold text-slate-700">
{res.titulo}
</p>
<p className="text-xs text-slate-500">{res.autor}</p> <p className="text-xs text-slate-500">{res.autor}</p>
</div> </div>
<Plus className="w-4 h-4 text-blue-600 opacity-0 group-hover:opacity-100 transition-opacity" /> <Plus className="h-4 w-4 text-blue-600 opacity-0 transition-opacity group-hover:opacity-100" />
</div> </div>
))} ))}
</div> </div>
</div> </div>
); )
} }

View File

@@ -1,10 +1,23 @@
import { useState } from 'react'; import { useEffect, useState } from 'react'
import { Plus, GripVertical, ChevronDown, ChevronRight, Edit3, Trash2, Clock, Save } from 'lucide-react'; import {
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; Plus,
import { Button } from '@/components/ui/button'; GripVertical,
import { Input } from '@/components/ui/input'; ChevronDown,
import { Badge } from '@/components/ui/badge'; ChevronRight,
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'; Edit3,
Trash2,
Clock,
Save,
} from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible'
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -14,24 +27,22 @@ import {
AlertDialogFooter, AlertDialogFooter,
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
} from '@/components/ui/alert-dialog'; } from '@/components/ui/alert-dialog'
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils'
//import { toast } from 'sonner'; //import { toast } from 'sonner';
export interface Tema { export interface Tema {
id: string; id: string
nombre: string; nombre: string
descripcion?: string; descripcion?: string
horasEstimadas?: number; horasEstimadas?: number
} }
export interface UnidadTematica { export interface UnidadTematica {
id: string; id: string
nombre: string; nombre: string
numero: number; numero: number
temas: Tema[]; temas: Tema[]
} }
const initialData: UnidadTematica[] = [ const initialData: UnidadTematica[] = [
@@ -42,152 +53,297 @@ const initialData: UnidadTematica[] = [
temas: [ temas: [
{ id: 't1', nombre: 'Tipos de IA y aplicaciones', horasEstimadas: 6 }, { id: 't1', nombre: 'Tipos de IA y aplicaciones', horasEstimadas: 6 },
{ id: 't2', nombre: 'Ética en IA', horasEstimadas: 3 }, { id: 't2', nombre: 'Ética en IA', horasEstimadas: 3 },
] ],
} },
]; ]
export function ContenidoTematico() { // Estructura que viene de tu JSON/API
const [unidades, setUnidades] = useState<UnidadTematica[]>(initialData); interface ContenidoApi {
const [expandedUnits, setExpandedUnits] = useState<Set<string>>(new Set(['u1'])); unidad: number
const [deleteDialog, setDeleteDialog] = useState<{ type: 'unidad' | 'tema'; id: string; parentId?: string } | null>(null); titulo: string
const [editingUnit, setEditingUnit] = useState<string | null>(null); temas: string[] | any[] // Acepta strings o objetos
const [editingTema, setEditingTema] = useState<{ unitId: string; temaId: string } | null>(null); [key: string]: any // Esta línea permite que haya más claves desconocidas
const [isSaving, setIsSaving] = useState(false); }
// Props del componente
interface ContenidoTematicoProps {
data: {
contenido_tematico: ContenidoApi[]
}
isLoading: boolean
}
export function ContenidoTematico({ data, isLoading }: ContenidoTematicoProps) {
const [unidades, setUnidades] = useState<UnidadTematica[]>([])
const [expandedUnits, setExpandedUnits] = useState<Set<string>>(
new Set(['u1']),
)
const [deleteDialog, setDeleteDialog] = useState<{
type: 'unidad' | 'tema'
id: string
parentId?: string
} | null>(null)
const [editingUnit, setEditingUnit] = useState<string | null>(null)
const [editingTema, setEditingTema] = useState<{
unitId: string
temaId: string
} | null>(null)
const [isSaving, setIsSaving] = useState(false)
useEffect(() => {
if (data?.contenido_tematico) {
const transformed = data.contenido_tematico.map(
(u: any, idx: number) => ({
id: `u-${idx}`,
numero: u.unidad || idx + 1,
nombre: u.titulo || 'Sin título',
temas: Array.isArray(u.temas)
? u.temas.map((t: any, tidx: number) => ({
id: `t-${idx}-${tidx}`,
nombre: typeof t === 'string' ? t : t.nombre || 'Tema',
horasEstimadas: t.horasEstimadas || 0,
}))
: [],
}),
)
setUnidades(transformed)
// Expandir la primera unidad automáticamente
if (transformed.length > 0) {
setExpandedUnits(new Set([transformed[0].id]))
}
}
}, [data])
if (isLoading)
return <div className="p-10 text-center">Cargando contenido...</div>
// 3. Cálculo de horas (ahora dinámico basado en los nuevos datos)
const totalHoras = unidades.reduce(
(acc, u) =>
acc + u.temas.reduce((sum, t) => sum + (t.horasEstimadas || 0), 0),
0,
)
// --- Lógica de Unidades --- // --- Lógica de Unidades ---
const toggleUnit = (id: string) => { const toggleUnit = (id: string) => {
const newExpanded = new Set(expandedUnits); const newExpanded = new Set(expandedUnits)
newExpanded.has(id) ? newExpanded.delete(id) : newExpanded.add(id); newExpanded.has(id) ? newExpanded.delete(id) : newExpanded.add(id)
setExpandedUnits(newExpanded); setExpandedUnits(newExpanded)
}; }
const addUnidad = () => { const addUnidad = () => {
const newId = `u-${Date.now()}`; const newId = `u-${Date.now()}`
const newUnidad: UnidadTematica = { const newUnidad: UnidadTematica = {
id: newId, id: newId,
nombre: 'Nueva Unidad', nombre: 'Nueva Unidad',
numero: unidades.length + 1, numero: unidades.length + 1,
temas: [], temas: [],
}; }
setUnidades([...unidades, newUnidad]); setUnidades([...unidades, newUnidad])
setExpandedUnits(new Set([...expandedUnits, newId])); setExpandedUnits(new Set([...expandedUnits, newId]))
setEditingUnit(newId); setEditingUnit(newId)
}; }
const updateUnidadNombre = (id: string, nombre: string) => { const updateUnidadNombre = (id: string, nombre: string) => {
setUnidades(unidades.map(u => u.id === id ? { ...u, nombre } : u)); setUnidades(unidades.map((u) => (u.id === id ? { ...u, nombre } : u)))
}; }
// --- Lógica de Temas --- // --- Lógica de Temas ---
const addTema = (unidadId: string) => { const addTema = (unidadId: string) => {
setUnidades(unidades.map(u => { setUnidades(
if (u.id === unidadId) { unidades.map((u) => {
const newTemaId = `t-${Date.now()}`; if (u.id === unidadId) {
const newTema: Tema = { id: newTemaId, nombre: 'Nuevo tema', horasEstimadas: 2 }; const newTemaId = `t-${Date.now()}`
setEditingTema({ unitId: unidadId, temaId: newTemaId }); const newTema: Tema = {
return { ...u, temas: [...u.temas, newTema] }; id: newTemaId,
} nombre: 'Nuevo tema',
return u; horasEstimadas: 2,
})); }
}; setEditingTema({ unitId: unidadId, temaId: newTemaId })
return { ...u, temas: [...u.temas, newTema] }
}
return u
}),
)
}
const updateTema = (unidadId: string, temaId: string, updates: Partial<Tema>) => { const updateTema = (
setUnidades(unidades.map(u => { unidadId: string,
if (u.id === unidadId) { temaId: string,
return { ...u, temas: u.temas.map(t => t.id === temaId ? { ...t, ...updates } : t) }; updates: Partial<Tema>,
} ) => {
return u; setUnidades(
})); unidades.map((u) => {
}; if (u.id === unidadId) {
return {
...u,
temas: u.temas.map((t) =>
t.id === temaId ? { ...t, ...updates } : t,
),
}
}
return u
}),
)
}
const handleDelete = () => { const handleDelete = () => {
if (!deleteDialog) return; if (!deleteDialog) return
if (deleteDialog.type === 'unidad') { if (deleteDialog.type === 'unidad') {
setUnidades(unidades.filter(u => u.id !== deleteDialog.id).map((u, i) => ({ ...u, numero: i + 1 }))); setUnidades(
unidades
.filter((u) => u.id !== deleteDialog.id)
.map((u, i) => ({ ...u, numero: i + 1 })),
)
} else if (deleteDialog.parentId) { } else if (deleteDialog.parentId) {
setUnidades(unidades.map(u => u.id === deleteDialog.parentId ? { ...u, temas: u.temas.filter(t => t.id !== deleteDialog.id) } : u)); setUnidades(
unidades.map((u) =>
u.id === deleteDialog.parentId
? { ...u, temas: u.temas.filter((t) => t.id !== deleteDialog.id) }
: u,
),
)
} }
setDeleteDialog(null); setDeleteDialog(null)
//toast.success("Eliminado correctamente"); //toast.success("Eliminado correctamente");
}; }
const totalHoras = unidades.reduce((acc, u) => acc + u.temas.reduce((sum, t) => sum + (t.horasEstimadas || 0), 0), 0);
return ( return (
<div className="max-w-5xl mx-auto py-10 space-y-6 animate-in fade-in duration-500"> <div className="animate-in fade-in mx-auto max-w-5xl space-y-6 py-10 duration-500">
<div className="flex items-center justify-between border-b pb-4"> <div className="flex items-center justify-between border-b pb-4">
<div> <div>
<h2 className="text-2xl font-bold tracking-tight text-slate-900">Contenido Temático</h2> <h2 className="text-2xl font-bold tracking-tight text-slate-900">
<p className="text-sm text-slate-500 mt-1"> Contenido Temático
</h2>
<p className="mt-1 text-sm text-slate-500">
{unidades.length} unidades {totalHoras} horas estimadas totales {unidades.length} unidades {totalHoras} horas estimadas totales
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="outline" onClick={addUnidad} className="gap-2"> <Button variant="outline" onClick={addUnidad} className="gap-2">
<Plus className="w-4 h-4" /> Nueva unidad <Plus className="h-4 w-4" /> Nueva unidad
</Button> </Button>
<Button onClick={() => { setIsSaving(true); setTimeout(() => { setIsSaving(false); /*toast.success("Guardado")*/; }, 1000); }} disabled={isSaving} className="bg-blue-600 hover:bg-blue-700"> <Button
<Save className="w-4 h-4 mr-2" /> {isSaving ? 'Guardando...' : 'Guardar'} onClick={() => {
setIsSaving(true)
setTimeout(() => {
setIsSaving(false) /*toast.success("Guardado")*/
}, 1000)
}}
disabled={isSaving}
className="bg-blue-600 hover:bg-blue-700"
>
<Save className="mr-2 h-4 w-4" />{' '}
{isSaving ? 'Guardando...' : 'Guardar'}
</Button> </Button>
</div> </div>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
{unidades.map((unidad) => ( {unidades.map((unidad) => (
<Card key={unidad.id} className="overflow-hidden border-slate-200 shadow-sm"> <Card
<Collapsible open={expandedUnits.has(unidad.id)} onOpenChange={() => toggleUnit(unidad.id)}> key={unidad.id}
<CardHeader className="bg-slate-50/50 py-3 border-b border-slate-100"> className="overflow-hidden border-slate-200 shadow-sm"
>
<Collapsible
open={expandedUnits.has(unidad.id)}
onOpenChange={() => toggleUnit(unidad.id)}
>
<CardHeader className="border-b border-slate-100 bg-slate-50/50 py-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<GripVertical className="w-4 h-4 text-slate-300 cursor-grab" /> <GripVertical className="h-4 w-4 cursor-grab text-slate-300" />
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<Button variant="ghost" size="sm" className="p-0 h-auto"> <Button variant="ghost" size="sm" className="h-auto p-0">
{expandedUnits.has(unidad.id) ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />} {expandedUnits.has(unidad.id) ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</Button> </Button>
</CollapsibleTrigger> </CollapsibleTrigger>
<Badge className="bg-blue-600 font-mono">Unidad {unidad.numero}</Badge> <Badge className="bg-blue-600 font-mono">
Unidad {unidad.numero}
</Badge>
{editingUnit === unidad.id ? ( {editingUnit === unidad.id ? (
<Input <Input
value={unidad.nombre} value={unidad.nombre}
onChange={(e) => updateUnidadNombre(unidad.id, e.target.value)} onChange={(e) =>
updateUnidadNombre(unidad.id, e.target.value)
}
onBlur={() => setEditingUnit(null)} onBlur={() => setEditingUnit(null)}
onKeyDown={(e) => e.key === 'Enter' && setEditingUnit(null)} onKeyDown={(e) =>
className="max-w-md h-8 bg-white" e.key === 'Enter' && setEditingUnit(null)
autoFocus }
className="h-8 max-w-md bg-white"
autoFocus
/> />
) : ( ) : (
<CardTitle className="text-base font-semibold cursor-pointer hover:text-blue-600 transition-colors" onClick={() => setEditingUnit(unidad.id)}> <CardTitle
className="cursor-pointer text-base font-semibold transition-colors hover:text-blue-600"
onClick={() => setEditingUnit(unidad.id)}
>
{unidad.nombre} {unidad.nombre}
</CardTitle> </CardTitle>
)} )}
<div className="ml-auto flex items-center gap-3"> <div className="ml-auto flex items-center gap-3">
<span className="text-xs font-medium text-slate-400 flex items-center gap-1"> <span className="flex items-center gap-1 text-xs font-medium text-slate-400">
<Clock className="w-3 h-3" /> {unidad.temas.reduce((sum, t) => sum + (t.horasEstimadas || 0), 0)}h <Clock className="h-3 w-3" />{' '}
{unidad.temas.reduce(
(sum, t) => sum + (t.horasEstimadas || 0),
0,
)}
h
</span> </span>
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-red-500" onClick={() => setDeleteDialog({ type: 'unidad', id: unidad.id })}> <Button
<Trash2 className="w-4 h-4" /> variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-red-500"
onClick={() =>
setDeleteDialog({ type: 'unidad', id: unidad.id })
}
>
<Trash2 className="h-4 w-4" />
</Button> </Button>
</div> </div>
</div> </div>
</CardHeader> </CardHeader>
<CollapsibleContent> <CollapsibleContent>
<CardContent className="pt-4 bg-white"> <CardContent className="bg-white pt-4">
<div className="space-y-1 ml-10 border-l-2 border-slate-50 pl-4"> <div className="ml-10 space-y-1 border-l-2 border-slate-50 pl-4">
{unidad.temas.map((tema, idx) => ( {unidad.temas.map((tema, idx) => (
<TemaRow <TemaRow
key={tema.id} key={tema.id}
tema={tema} tema={tema}
index={idx + 1} index={idx + 1}
isEditing={editingTema?.unitId === unidad.id && editingTema?.temaId === tema.id} isEditing={
onEdit={() => setEditingTema({ unitId: unidad.id, temaId: tema.id })} editingTema?.unitId === unidad.id &&
editingTema?.temaId === tema.id
}
onEdit={() =>
setEditingTema({ unitId: unidad.id, temaId: tema.id })
}
onStopEditing={() => setEditingTema(null)} onStopEditing={() => setEditingTema(null)}
onUpdate={(updates) => updateTema(unidad.id, tema.id, updates)} onUpdate={(updates) =>
onDelete={() => setDeleteDialog({ type: 'tema', id: tema.id, parentId: unidad.id })} updateTema(unidad.id, tema.id, updates)
}
onDelete={() =>
setDeleteDialog({
type: 'tema',
id: tema.id,
parentId: unidad.id,
})
}
/> />
))} ))}
<Button variant="ghost" size="sm" className="text-blue-600 hover:text-blue-700 hover:bg-blue-50 w-full justify-start mt-2" onClick={() => addTema(unidad.id)}> <Button
<Plus className="w-3 h-3 mr-2" /> Añadir subtema variant="ghost"
size="sm"
className="mt-2 w-full justify-start text-blue-600 hover:bg-blue-50 hover:text-blue-700"
onClick={() => addTema(unidad.id)}
>
<Plus className="mr-2 h-3 w-3" /> Añadir subtema
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
@@ -197,81 +353,137 @@ export function ContenidoTematico() {
))} ))}
</div> </div>
<DeleteConfirmDialog dialog={deleteDialog} setDialog={setDeleteDialog} onConfirm={handleDelete} /> <DeleteConfirmDialog
dialog={deleteDialog}
setDialog={setDeleteDialog}
onConfirm={handleDelete}
/>
</div> </div>
); )
} }
// --- Componentes Auxiliares --- // --- Componentes Auxiliares ---
interface TemaRowProps { interface TemaRowProps {
tema: Tema; tema: Tema
index: number; index: number
isEditing: boolean; isEditing: boolean
onEdit: () => void; onEdit: () => void
onStopEditing: () => void; onStopEditing: () => void
onUpdate: (updates: Partial<Tema>) => void; onUpdate: (updates: Partial<Tema>) => void
onDelete: () => void; onDelete: () => void
} }
function TemaRow({ tema, index, isEditing, onEdit, onStopEditing, onUpdate, onDelete }: TemaRowProps) { function TemaRow({
tema,
index,
isEditing,
onEdit,
onStopEditing,
onUpdate,
onDelete,
}: TemaRowProps) {
return ( return (
<div className={cn("flex items-center gap-3 p-2 rounded-md group transition-all", isEditing ? "bg-blue-50 ring-1 ring-blue-100" : "hover:bg-slate-50")}> <div
<span className="text-xs font-mono text-slate-400 w-4">{index}.</span> className={cn(
'group flex items-center gap-3 rounded-md p-2 transition-all',
isEditing ? 'bg-blue-50 ring-1 ring-blue-100' : 'hover:bg-slate-50',
)}
>
<span className="w-4 font-mono text-xs text-slate-400">{index}.</span>
{isEditing ? ( {isEditing ? (
<div className="flex-1 flex items-center gap-2 animate-in slide-in-from-left-2"> <div className="animate-in slide-in-from-left-2 flex flex-1 items-center gap-2">
<Input value={tema.nombre} onChange={(e) => onUpdate({ nombre: e.target.value })} className="h-8 flex-1 bg-white" placeholder="Nombre" autoFocus /> <Input
<Input type="number" value={tema.horasEstimadas} onChange={(e) => onUpdate({ horasEstimadas: parseInt(e.target.value) || 0 })} className="h-8 w-16 bg-white" /> value={tema.nombre}
<Button size="sm" className="bg-emerald-600 h-8" onClick={onStopEditing}>Listo</Button> onChange={(e) => onUpdate({ nombre: e.target.value })}
className="h-8 flex-1 bg-white"
placeholder="Nombre"
autoFocus
/>
<Input
type="number"
value={tema.horasEstimadas}
onChange={(e) =>
onUpdate({ horasEstimadas: parseInt(e.target.value) || 0 })
}
className="h-8 w-16 bg-white"
/>
<Button
size="sm"
className="h-8 bg-emerald-600"
onClick={onStopEditing}
>
Listo
</Button>
</div> </div>
) : ( ) : (
<> <>
<div className="flex-1 cursor-pointer" onClick={onEdit}> <div className="flex-1 cursor-pointer" onClick={onEdit}>
<p className="text-sm font-medium text-slate-700">{tema.nombre}</p> <p className="text-sm font-medium text-slate-700">{tema.nombre}</p>
</div> </div>
<Badge variant="secondary" className="text-[10px] opacity-60">{tema.horasEstimadas}h</Badge> <Badge variant="secondary" className="text-[10px] opacity-60">
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity"> {tema.horasEstimadas}h
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-blue-600" onClick={onEdit}><Edit3 className="w-3 h-3" /></Button> </Badge>
<Button variant="ghost" size="icon" className="h-7 w-7 text-slate-400 hover:text-red-500" onClick={onDelete}><Trash2 className="w-3 h-3" /></Button> <div className="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100">
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-slate-400 hover:text-blue-600"
onClick={onEdit}
>
<Edit3 className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-slate-400 hover:text-red-500"
onClick={onDelete}
>
<Trash2 className="h-3 w-3" />
</Button>
</div> </div>
</> </>
)} )}
</div> </div>
); )
} }
interface DeleteDialogState { interface DeleteDialogState {
type: 'unidad' | 'tema'; type: 'unidad' | 'tema'
id: string; id: string
parentId?: string; parentId?: string
} }
interface DeleteConfirmDialogProps { interface DeleteConfirmDialogProps {
dialog: DeleteDialogState | null; dialog: DeleteDialogState | null
setDialog: (value: DeleteDialogState | null) => void; setDialog: (value: DeleteDialogState | null) => void
onConfirm: () => void; onConfirm: () => void
} }
function DeleteConfirmDialog({ function DeleteConfirmDialog({
dialog, dialog,
setDialog, setDialog,
onConfirm, onConfirm,
}: DeleteConfirmDialogProps) { }: DeleteConfirmDialogProps) {
return ( return (
<AlertDialog open={!!dialog} onOpenChange={() => setDialog(null)}> <AlertDialog open={!!dialog} onOpenChange={() => setDialog(null)}>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>¿Confirmar eliminación?</AlertDialogTitle> <AlertDialogTitle>¿Confirmar eliminación?</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
Estás a punto de borrar un {dialog?.type}. Esta acción no se puede deshacer. Estás a punto de borrar un {dialog?.type}. Esta acción no se puede
deshacer.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancelar</AlertDialogCancel> <AlertDialogCancel>Cancelar</AlertDialogCancel>
<AlertDialogAction onClick={onConfirm} className="bg-red-600 hover:bg-red-700 text-white">Eliminar</AlertDialogAction> <AlertDialogAction
onClick={onConfirm}
className="bg-red-600 text-white hover:bg-red-700"
>
Eliminar
</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
); )
} }

View File

@@ -1,8 +1,16 @@
import { useState } from 'react'; import { useState } from 'react'
import { FileText, Download, RefreshCw, Calendar, FileCheck, AlertTriangle, Loader2 } from 'lucide-react'; import {
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; FileText,
import { Button } from '@/components/ui/button'; Download,
import { Badge } from '@/components/ui/badge'; RefreshCw,
Calendar,
FileCheck,
AlertTriangle,
Loader2,
} from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -13,63 +21,88 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from '@/components/ui/alert-dialog'; } from '@/components/ui/alert-dialog'
import type { DocumentoMateria, Materia, MateriaStructure } from '@/types/materia'; import type {
import { cn } from '@/lib/utils'; DocumentoMateria,
Materia,
MateriaStructure,
} from '@/types/materia'
import { cn } from '@/lib/utils'
import { useSubjectBibliografia } from '@/data/hooks/useSubjects'
//import { toast } from 'sonner'; //import { toast } from 'sonner';
//import { format } from 'date-fns'; //import { format } from 'date-fns';
//import { es } from 'date-fns/locale'; //import { es } from 'date-fns/locale';
interface DocumentoSEPTabProps { interface DocumentoSEPTabProps {
documento: DocumentoMateria | null; documento: DocumentoMateria | null
materia: Materia; materia: Materia
estructura: MateriaStructure; estructura: MateriaStructure
datosGenerales: Record<string, any>; datosGenerales: Record<string, any>
onRegenerate: () => void; onRegenerate: () => void
isRegenerating: boolean; isRegenerating: boolean
} }
export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales, onRegenerate, isRegenerating }: DocumentoSEPTabProps) { export function DocumentoSEPTab({
const [showConfirmDialog, setShowConfirmDialog] = useState(false); documento,
materia,
estructura,
datosGenerales,
onRegenerate,
isRegenerating,
}: DocumentoSEPTabProps) {
const [showConfirmDialog, setShowConfirmDialog] = useState(false)
// Check completeness // Check completeness
const camposObligatorios = estructura.campos.filter(c => c.obligatorio); const camposObligatorios = estructura.campos.filter((c) => c.obligatorio)
const camposCompletos = camposObligatorios.filter(c => datosGenerales[c.id]?.trim()); const camposCompletos = camposObligatorios.filter((c) =>
const completeness = Math.round((camposCompletos.length / camposObligatorios.length) * 100); datosGenerales[c.id]?.trim(),
const isComplete = completeness === 100; )
const completeness = Math.round(
(camposCompletos.length / camposObligatorios.length) * 100,
)
const isComplete = completeness === 100
const handleRegenerate = () => { const handleRegenerate = () => {
setShowConfirmDialog(false); setShowConfirmDialog(false)
onRegenerate(); onRegenerate()
//toast.success('Regenerando documento...'); //toast.success('Regenerando documento...');
}; }
return ( return (
<div className="space-y-6 animate-fade-in"> <div className="animate-fade-in space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="font-display text-2xl font-semibold text-foreground flex items-center gap-2"> <h2 className="font-display text-foreground flex items-center gap-2 text-2xl font-semibold">
<FileCheck className="w-6 h-6 text-accent" /> <FileCheck className="text-accent h-6 w-6" />
Documento SEP Documento SEP
</h2> </h2>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-muted-foreground mt-1 text-sm">
Previsualización del documento oficial para la SEP Previsualización del documento oficial para la SEP
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{documento?.estado === 'listo' && ( {documento?.estado === 'listo' && (
<Button variant="outline" onClick={() => console.log("descargando") /*toast.info('Descarga iniciada')*/}> <Button
<Download className="w-4 h-4 mr-2" /> variant="outline"
onClick={
() =>
console.log('descargando') /*toast.info('Descarga iniciada')*/
}
>
<Download className="mr-2 h-4 w-4" />
Descargar Descargar
</Button> </Button>
)} )}
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}> <AlertDialog
open={showConfirmDialog}
onOpenChange={setShowConfirmDialog}
>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button disabled={isRegenerating || !isComplete}> <Button disabled={isRegenerating || !isComplete}>
{isRegenerating ? ( {isRegenerating ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> <Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : ( ) : (
<RefreshCw className="w-4 h-4 mr-2" /> <RefreshCw className="mr-2 h-4 w-4" />
)} )}
{isRegenerating ? 'Generando...' : 'Regenerar documento'} {isRegenerating ? 'Generando...' : 'Regenerar documento'}
</Button> </Button>
@@ -78,8 +111,9 @@ export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>¿Regenerar documento SEP?</AlertDialogTitle> <AlertDialogTitle>¿Regenerar documento SEP?</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
Se creará una nueva versión del documento con los datos actuales de la materia. Se creará una nueva versión del documento con los datos
La versión anterior quedará en el historial. actuales de la materia. La versión anterior quedará en el
historial.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
@@ -93,91 +127,108 @@ export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales
</div> </div>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
{/* Document preview */} {/* Document preview */}
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<Card className="card-elevated h-[700px] overflow-hidden"> <Card className="card-elevated h-[700px] overflow-hidden">
{documento?.estado === 'listo' ? ( {documento?.estado === 'listo' ? (
<div className="h-full bg-muted/30 flex flex-col"> <div className="bg-muted/30 flex h-full flex-col">
{/* Simulated document header */} {/* Simulated document header */}
<div className="bg-card border-b p-4"> <div className="bg-card border-b p-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<FileText className="w-5 h-5 text-primary" /> <FileText className="text-primary h-5 w-5" />
<span className="font-medium text-foreground"> <span className="text-foreground font-medium">
Programa de Estudios - {materia.clave} Programa de Estudios - {materia.clave}
</span> </span>
</div> </div>
<Badge variant="outline">Versión {documento.version}</Badge> <Badge variant="outline">Versión {documento.version}</Badge>
</div> </div>
</div> </div>
{/* Document content simulation */} {/* Document content simulation */}
<div className="flex-1 overflow-y-auto p-8"> <div className="flex-1 overflow-y-auto p-8">
<div className="max-w-2xl mx-auto bg-card rounded-lg shadow-lg p-8 space-y-6"> <div className="bg-card mx-auto max-w-2xl space-y-6 rounded-lg p-8 shadow-lg">
{/* Header */} {/* Header */}
<div className="text-center border-b pb-6"> <div className="border-b pb-6 text-center">
<p className="text-xs text-muted-foreground uppercase tracking-wide mb-2"> <p className="text-muted-foreground mb-2 text-xs tracking-wide uppercase">
Secretaría de Educación Pública Secretaría de Educación Pública
</p> </p>
<h1 className="font-display text-2xl font-bold text-primary mb-1"> <h1 className="font-display text-primary mb-1 text-2xl font-bold">
{materia.nombre} {materia.nombre}
</h1> </h1>
<p className="text-sm text-muted-foreground"> <p className="text-muted-foreground text-sm">
Clave: {materia.clave} | Créditos: {materia.creditos || 'N/A'} Clave: {materia.clave} | Créditos:{' '}
{materia.creditos || 'N/A'}
</p> </p>
</div> </div>
{/* Datos de la institución */} {/* Datos de la institución */}
<div className="space-y-1 text-sm"> <div className="space-y-1 text-sm">
<p><strong>Carrera:</strong> {materia.carrera}</p> <p>
<p><strong>Facultad:</strong> {materia.facultad}</p> <strong>Carrera:</strong> {materia.carrera}
<p><strong>Plan de estudios:</strong> {materia.planNombre}</p> </p>
{materia.ciclo && <p><strong>Ciclo:</strong> {materia.ciclo}</p>} <p>
<strong>Facultad:</strong> {materia.facultad}
</p>
<p>
<strong>Plan de estudios:</strong> {materia.planNombre}
</p>
{materia.ciclo && (
<p>
<strong>Ciclo:</strong> {materia.ciclo}
</p>
)}
</div> </div>
{/* Campos del documento */} {/* Campos del documento */}
{estructura.campos.map((campo) => { {estructura.campos.map((campo) => {
const valor = datosGenerales[campo.id]; const valor = datosGenerales[campo.id]
if (!valor) return null; if (!valor) return null
return ( return (
<div key={campo.id} className="space-y-2"> <div key={campo.id} className="space-y-2">
<h3 className="font-semibold text-foreground border-b pb-1"> <h3 className="text-foreground border-b pb-1 font-semibold">
{campo.nombre} {campo.nombre}
</h3> </h3>
<p className="text-sm text-foreground whitespace-pre-wrap leading-relaxed"> <p className="text-foreground text-sm leading-relaxed whitespace-pre-wrap">
{valor} {valor}
</p> </p>
</div> </div>
); )
})} })}
{/* Footer */} {/* Footer */}
<div className="border-t pt-6 mt-8 text-center text-xs text-muted-foreground"> <div className="text-muted-foreground mt-8 border-t pt-6 text-center text-xs">
<p>Documento generado el {/*format(documento.fechaGeneracion, "d 'de' MMMM 'de' yyyy", { locale: es })*/}</p> <p>
Documento generado el{' '}
{/*format(documento.fechaGeneracion, "d 'de' MMMM 'de' yyyy", { locale: es })*/}
</p>
<p className="mt-1">Universidad La Salle</p> <p className="mt-1">Universidad La Salle</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
) : documento?.estado === 'generando' ? ( ) : documento?.estado === 'generando' ? (
<div className="h-full flex items-center justify-center"> <div className="flex h-full items-center justify-center">
<div className="text-center"> <div className="text-center">
<Loader2 className="w-12 h-12 mx-auto text-accent animate-spin mb-4" /> <Loader2 className="text-accent mx-auto mb-4 h-12 w-12 animate-spin" />
<p className="text-muted-foreground">Generando documento...</p> <p className="text-muted-foreground">
Generando documento...
</p>
</div> </div>
</div> </div>
) : ( ) : (
<div className="h-full flex items-center justify-center"> <div className="flex h-full items-center justify-center">
<div className="text-center max-w-sm"> <div className="max-w-sm text-center">
<FileText className="w-12 h-12 mx-auto text-muted-foreground/50 mb-4" /> <FileText className="text-muted-foreground/50 mx-auto mb-4 h-12 w-12" />
<p className="text-muted-foreground mb-4"> <p className="text-muted-foreground mb-4">
No hay documento generado aún No hay documento generado aún
</p> </p>
{!isComplete && ( {!isComplete && (
<div className="p-4 bg-warning/10 rounded-lg text-sm text-warning-foreground"> <div className="bg-warning/10 text-warning-foreground rounded-lg p-4 text-sm">
<AlertTriangle className="w-4 h-4 inline mr-2" /> <AlertTriangle className="mr-2 inline h-4 w-4" />
Completa todos los campos obligatorios para generar el documento Completa todos los campos obligatorios para generar el
documento
</div> </div>
)} )}
</div> </div>
@@ -191,28 +242,41 @@ export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales
{/* Status */} {/* Status */}
<Card className="card-elevated"> <Card className="card-elevated">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Estado del documento</CardTitle> <CardTitle className="text-sm font-medium">
Estado del documento
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
{documento && ( {documento && (
<> <>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Versión</span> <span className="text-muted-foreground text-sm">
Versión
</span>
<Badge variant="outline">{documento.version}</Badge> <Badge variant="outline">{documento.version}</Badge>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Generado</span> <span className="text-muted-foreground text-sm">
Generado
</span>
<span className="text-sm"> <span className="text-sm">
{/*format(documento.fechaGeneracion, "d MMM yyyy, HH:mm", { locale: es })*/} {/*format(documento.fechaGeneracion, "d MMM yyyy, HH:mm", { locale: es })*/}
</span> </span>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Estado</span> <span className="text-muted-foreground text-sm">
<Badge className={cn( Estado
documento.estado === 'listo' && "bg-success text-success-foreground", </span>
documento.estado === 'generando' && "bg-info text-info-foreground", <Badge
documento.estado === 'error' && "bg-destructive text-destructive-foreground" className={cn(
)}> documento.estado === 'listo' &&
'bg-success text-success-foreground',
documento.estado === 'generando' &&
'bg-info text-info-foreground',
documento.estado === 'error' &&
'bg-destructive text-destructive-foreground',
)}
>
{documento.estado === 'listo' && 'Listo'} {documento.estado === 'listo' && 'Listo'}
{documento.estado === 'generando' && 'Generando'} {documento.estado === 'generando' && 'Generando'}
{documento.estado === 'error' && 'Error'} {documento.estado === 'error' && 'Error'}
@@ -226,44 +290,60 @@ export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales
{/* Completeness */} {/* Completeness */}
<Card className="card-elevated"> <Card className="card-elevated">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Completitud de datos</CardTitle> <CardTitle className="text-sm font-medium">
Completitud de datos
</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between text-sm"> <div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Campos obligatorios</span> <span className="text-muted-foreground">
<span className="font-medium">{camposCompletos.length}/{camposObligatorios.length}</span> Campos obligatorios
</span>
<span className="font-medium">
{camposCompletos.length}/{camposObligatorios.length}
</span>
</div> </div>
<div className="h-2 bg-muted rounded-full overflow-hidden"> <div className="bg-muted h-2 overflow-hidden rounded-full">
<div <div
className={cn( className={cn(
"h-full transition-all duration-500", 'h-full transition-all duration-500',
completeness === 100 ? "bg-success" : "bg-accent" completeness === 100 ? 'bg-success' : 'bg-accent',
)} )}
style={{ width: `${completeness}%` }} style={{ width: `${completeness}%` }}
/> />
</div> </div>
<p className={cn( <p
"text-xs", className={cn(
completeness === 100 ? "text-success" : "text-muted-foreground" 'text-xs',
)}> completeness === 100
{completeness === 100 ? 'text-success'
: 'text-muted-foreground',
)}
>
{completeness === 100
? 'Todos los campos obligatorios están completos' ? 'Todos los campos obligatorios están completos'
: `Faltan ${camposObligatorios.length - camposCompletos.length} campos por completar` : `Faltan ${camposObligatorios.length - camposCompletos.length} campos por completar`}
}
</p> </p>
</div> </div>
{/* Missing fields */} {/* Missing fields */}
{!isComplete && ( {!isComplete && (
<div className="space-y-2"> <div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">Campos faltantes:</p> <p className="text-muted-foreground text-xs font-medium">
{camposObligatorios.filter(c => !datosGenerales[c.id]?.trim()).map((campo) => ( Campos faltantes:
<div key={campo.id} className="flex items-center gap-2 text-sm"> </p>
<AlertTriangle className="w-3 h-3 text-warning" /> {camposObligatorios
<span className="text-foreground">{campo.nombre}</span> .filter((c) => !datosGenerales[c.id]?.trim())
</div> .map((campo) => (
))} <div
key={campo.id}
className="flex items-center gap-2 text-sm"
>
<AlertTriangle className="text-warning h-3 w-3" />
<span className="text-foreground">{campo.nombre}</span>
</div>
))}
</div> </div>
)} )}
</CardContent> </CardContent>
@@ -272,36 +352,62 @@ export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales
{/* Requirements */} {/* Requirements */}
<Card className="card-elevated"> <Card className="card-elevated">
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">Requisitos SEP</CardTitle> <CardTitle className="text-sm font-medium">
Requisitos SEP
</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<ul className="space-y-2 text-sm"> <ul className="space-y-2 text-sm">
<li className="flex items-start gap-2"> <li className="flex items-start gap-2">
<div className={cn( <div
"w-4 h-4 rounded-full flex items-center justify-center mt-0.5", className={cn(
datosGenerales['objetivo_general'] ? "bg-success/20" : "bg-muted" 'mt-0.5 flex h-4 w-4 items-center justify-center rounded-full',
)}> datosGenerales['objetivo_general']
{datosGenerales['objetivo_general'] && <Check className="w-3 h-3 text-success" />} ? 'bg-success/20'
: 'bg-muted',
)}
>
{datosGenerales['objetivo_general'] && (
<Check className="text-success h-3 w-3" />
)}
</div> </div>
<span className="text-muted-foreground">Objetivo general definido</span> <span className="text-muted-foreground">
Objetivo general definido
</span>
</li> </li>
<li className="flex items-start gap-2"> <li className="flex items-start gap-2">
<div className={cn( <div
"w-4 h-4 rounded-full flex items-center justify-center mt-0.5", className={cn(
datosGenerales['competencias'] ? "bg-success/20" : "bg-muted" 'mt-0.5 flex h-4 w-4 items-center justify-center rounded-full',
)}> datosGenerales['competencias']
{datosGenerales['competencias'] && <Check className="w-3 h-3 text-success" />} ? 'bg-success/20'
: 'bg-muted',
)}
>
{datosGenerales['competencias'] && (
<Check className="text-success h-3 w-3" />
)}
</div> </div>
<span className="text-muted-foreground">Competencias especificadas</span> <span className="text-muted-foreground">
Competencias especificadas
</span>
</li> </li>
<li className="flex items-start gap-2"> <li className="flex items-start gap-2">
<div className={cn( <div
"w-4 h-4 rounded-full flex items-center justify-center mt-0.5", className={cn(
datosGenerales['evaluacion'] ? "bg-success/20" : "bg-muted" 'mt-0.5 flex h-4 w-4 items-center justify-center rounded-full',
)}> datosGenerales['evaluacion']
{datosGenerales['evaluacion'] && <Check className="w-3 h-3 text-success" />} ? 'bg-success/20'
: 'bg-muted',
)}
>
{datosGenerales['evaluacion'] && (
<Check className="text-success h-3 w-3" />
)}
</div> </div>
<span className="text-muted-foreground">Criterios de evaluación</span> <span className="text-muted-foreground">
Criterios de evaluación
</span>
</li> </li>
</ul> </ul>
</CardContent> </CardContent>
@@ -309,13 +415,19 @@ export function DocumentoSEPTab({ documento, materia, estructura, datosGenerales
</div> </div>
</div> </div>
</div> </div>
); )
} }
function Check({ className }: { className?: string }) { function Check({ className }: { className?: string }) {
return ( return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3"> <svg
className={className}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="3"
>
<polyline points="20 6 9 17 4 12" /> <polyline points="20 6 9 17 4 12" />
</svg> </svg>
); )
} }

View File

@@ -1,196 +1,354 @@
import { useState } from 'react'; import { useState, useMemo } from 'react'
import { History, FileText, List, BookMarked, Sparkles, FileCheck, User, Filter, Calendar } from 'lucide-react'; import {
import { Card, CardContent } from '@/components/ui/card'; History,
import { Button } from '@/components/ui/button'; FileText,
import { Badge } from '@/components/ui/badge'; List,
BookMarked,
Sparkles,
FileCheck,
User,
Filter,
Calendar,
Loader2,
Eye,
} from 'lucide-react'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuCheckboxItem, DropdownMenuCheckboxItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu'
import type { CambioMateria } from '@/types/materia'; import { cn } from '@/lib/utils'
import { cn } from '@/lib/utils'; import { format, parseISO } from 'date-fns'
import { format, formatDistanceToNow } from 'date-fns'; import { es } from 'date-fns/locale'
import { es } from 'date-fns/locale'; import { useSubjectHistorial } from '@/data/hooks/useSubjects'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
interface HistorialTabProps { const tipoConfig: Record<string, { label: string; icon: any; color: string }> =
historial: CambioMateria[]; {
} datos: { label: 'Datos generales', icon: FileText, color: 'text-info' },
contenido: {
label: 'Contenido temático',
icon: List,
color: 'text-accent',
},
bibliografia: {
label: 'Bibliografía',
icon: BookMarked,
color: 'text-success',
},
ia: { label: 'IA', icon: Sparkles, color: 'text-amber-500' },
documento: {
label: 'Documento SEP',
icon: FileCheck,
color: 'text-primary',
},
}
const tipoConfig: Record<string, { label: string; icon: React.ComponentType<{ className?: string }>; color: string }> = { export function HistorialTab() {
datos: { label: 'Datos generales', icon: FileText, color: 'text-info' }, // 1. Obtenemos los datos directamente dentro del componente
contenido: { label: 'Contenido temático', icon: List, color: 'text-accent' }, const { data: rawData, isLoading } = useSubjectHistorial(
bibliografia: { label: 'Bibliografía', icon: BookMarked, color: 'text-success' }, '9d4dda6a-488f-428a-8a07-38081592a641',
ia: { label: 'IA', icon: Sparkles, color: 'text-amber-500' }, )
documento: { label: 'Documento SEP', icon: FileCheck, color: 'text-primary' },
};
export function HistorialTab({ historial }: HistorialTabProps) { const [filtros, setFiltros] = useState<Set<string>>(
const [filtros, setFiltros] = useState<Set<string>>(new Set(['datos', 'contenido', 'bibliografia', 'ia', 'documento'])); new Set(['datos', 'contenido', 'bibliografia', 'ia', 'documento']),
)
// ESTADOS PARA EL MODAL
const [selectedChange, setSelectedChange] = useState<any>(null)
const [isModalOpen, setIsModalOpen] = useState(false)
const RenderValue = ({ value }: { value: any }) => {
// 1. Caso: Nulo o vacío
if (
value === null ||
value === undefined ||
value === 'Sin información previa'
) {
return (
<span className="text-muted-foreground italic">Sin información</span>
)
}
// 2. Caso: Es un ARRAY (como tu lista de unidades)
if (Array.isArray(value)) {
return (
<div className="space-y-4">
{value.map((item, index) => (
<div
key={index}
className="rounded-lg border bg-white/50 p-3 shadow-sm"
>
<RenderValue value={item} />
</div>
))}
</div>
)
}
// 3. Caso: Es un OBJETO (como cada unidad con titulo, temas, etc.)
if (typeof value === 'object') {
return (
<div className="grid gap-2">
{Object.entries(value).map(([key, val]) => (
<div key={key} className="flex flex-col">
<span className="text-[10px] font-bold tracking-wider text-slate-400 uppercase">
{key.replace(/_/g, ' ')}
</span>
<div className="text-sm text-slate-700">
{/* Llamada recursiva para manejar lo que haya dentro del valor */}
{typeof val === 'object' ? (
<div className="mt-1 border-l-2 border-slate-100 pl-2">
<RenderValue value={val} />
</div>
) : (
String(val)
)}
</div>
</div>
))}
</div>
)
}
// 4. Caso: Texto o número simple
return <span className="text-sm leading-relaxed">{String(value)}</span>
}
const historialTransformado = useMemo(() => {
if (!rawData) return []
return rawData.map((item: any) => ({
id: item.id,
tipo: item.campo === 'contenido_tematico' ? 'contenido' : 'datos',
descripcion: `Se actualizó el campo ${item.campo.replace('_', ' ')}`,
fecha: parseISO(item.cambiado_en),
usuario: item.fuente === 'HUMANO' ? 'Usuario Staff' : 'Sistema IA',
detalles: {
campo: item.campo,
valor_anterior: item.valor_anterior || 'Sin datos previos', // Asumiendo que existe en tu API
valor_nuevo: item.valor_nuevo,
},
}))
}, [rawData])
const openCompareModal = (cambio: any) => {
setSelectedChange(cambio)
setIsModalOpen(true)
}
const toggleFiltro = (tipo: string) => { const toggleFiltro = (tipo: string) => {
const newFiltros = new Set(filtros); const newFiltros = new Set(filtros)
if (newFiltros.has(tipo)) { if (newFiltros.has(tipo)) newFiltros.delete(tipo)
newFiltros.delete(tipo); else newFiltros.add(tipo)
} else { setFiltros(newFiltros)
newFiltros.add(tipo); }
}
setFiltros(newFiltros);
};
const filteredHistorial = historial.filter(cambio => filtros.has(cambio.tipo)); // 3. Aplicamos filtros y agrupamiento sobre los datos transformados
const filteredHistorial = historialTransformado.filter((cambio) =>
filtros.has(cambio.tipo),
)
// Group by date const groupedHistorial = filteredHistorial.reduce(
const groupedHistorial = filteredHistorial.reduce((groups, cambio) => { (groups, cambio) => {
const dateKey = format(cambio.fecha, 'yyyy-MM-dd'); const dateKey = format(cambio.fecha, 'yyyy-MM-dd')
if (!groups[dateKey]) { if (!groups[dateKey]) groups[dateKey] = []
groups[dateKey] = []; groups[dateKey].push(cambio)
} return groups
groups[dateKey].push(cambio); },
return groups; {} as Record<string, any[]>,
}, {} as Record<string, CambioMateria[]>); )
const sortedDates = Object.keys(groupedHistorial).sort((a, b) => b.localeCompare(a)); const sortedDates = Object.keys(groupedHistorial).sort((a, b) =>
b.localeCompare(a),
)
if (isLoading) {
return (
<div className="flex h-48 items-center justify-center">
<Loader2 className="text-primary h-8 w-8 animate-spin" />
</div>
)
}
return ( return (
<div className="space-y-6 animate-fade-in"> <div className="animate-fade-in space-y-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="font-display text-2xl font-semibold text-foreground flex items-center gap-2"> <h2 className="font-display text-foreground flex items-center gap-2 text-2xl font-semibold">
<History className="w-6 h-6 text-accent" /> <History className="text-accent h-6 w-6" />
Historial de cambios Historial de cambios
</h2> </h2>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-muted-foreground mt-1 text-sm">
{historial.length} cambios registrados {historialTransformado.length} cambios registrados
</p> </p>
</div> </div>
{/* Dropdown de Filtros (Igual al anterior) */}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline"> <Button variant="outline">
<Filter className="w-4 h-4 mr-2" /> <Filter className="mr-2 h-4 w-4" />
Filtrar ({filtros.size}) Filtrar ({filtros.size})
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48"> <DropdownMenuContent align="end" className="w-48">
{Object.entries(tipoConfig).map(([tipo, config]) => { {Object.entries(tipoConfig).map(([tipo, config]) => (
const Icon = config.icon; <DropdownMenuCheckboxItem
return ( key={tipo}
<DropdownMenuCheckboxItem checked={filtros.has(tipo)}
key={tipo} onCheckedChange={() => toggleFiltro(tipo)}
checked={filtros.has(tipo)} >
onCheckedChange={() => toggleFiltro(tipo)} <config.icon className={cn('mr-2 h-4 w-4', config.color)} />
> {config.label}
<Icon className={cn("w-4 h-4 mr-2", config.color)} /> </DropdownMenuCheckboxItem>
{config.label} ))}
</DropdownMenuCheckboxItem>
);
})}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
{filteredHistorial.length === 0 ? ( {filteredHistorial.length === 0 ? (
<Card className="card-elevated"> <Card>
<CardContent className="py-12 text-center"> <CardContent className="py-12 text-center">
<History className="w-12 h-12 mx-auto text-muted-foreground/50 mb-4" /> <History className="text-muted-foreground/50 mx-auto mb-4 h-12 w-12" />
<p className="text-muted-foreground"> <p className="text-muted-foreground">No se encontraron cambios.</p>
{historial.length === 0
? 'No hay cambios registrados aún'
: 'No hay cambios con los filtros seleccionados'
}
</p>
</CardContent> </CardContent>
</Card> </Card>
) : ( ) : (
<div className="space-y-8"> <div className="space-y-8">
{sortedDates.map((dateKey) => { {sortedDates.map((dateKey) => (
const cambios = groupedHistorial[dateKey]; <div key={dateKey}>
const date = new Date(dateKey); <div className="mb-4 flex items-center gap-3">
const isToday = format(new Date(), 'yyyy-MM-dd') === dateKey; <Calendar className="text-muted-foreground h-4 w-4" />
const isYesterday = format(new Date(Date.now() - 86400000), 'yyyy-MM-dd') === dateKey; <h3 className="text-foreground font-semibold">
{format(parseISO(dateKey), "EEEE, d 'de' MMMM", {
locale: es,
})}
</h3>
</div>
return ( <div className="border-border ml-4 space-y-4 border-l-2 pl-6">
<div key={dateKey}> {groupedHistorial[dateKey].map((cambio) => {
{/* Date header */} const config = tipoConfig[cambio.tipo] || tipoConfig.datos
<div className="flex items-center gap-3 mb-4"> const Icon = config.icon
<div className="p-2 rounded-lg bg-muted"> return (
<Calendar className="w-4 h-4 text-muted-foreground" /> <div key={cambio.id} className="relative">
</div> <div
<div> className={cn(
<h3 className="font-semibold text-foreground"> 'border-background absolute -left-[31px] h-4 w-4 rounded-full border-2',
{isToday ? 'Hoy' : isYesterday ? 'Ayer' : format(date, "EEEE, d 'de' MMMM", { locale: es })} `bg-current ${config.color}`,
</h3> )}
<p className="text-xs text-muted-foreground"> />
{cambios.length} {cambios.length === 1 ? 'cambio' : 'cambios'} <Card className="card-interactive">
</p> <CardContent className="py-4">
</div> <div className="flex items-start gap-4">
</div> <div
className={cn(
{/* Timeline */} 'bg-muted rounded-lg p-2',
<div className="ml-4 border-l-2 border-border pl-6 space-y-4"> config.color,
{cambios.map((cambio) => { )}
const config = tipoConfig[cambio.tipo]; >
const Icon = config.icon; <Icon className="h-4 w-4" />
return ( </div>
<div key={cambio.id} className="relative"> <div className="flex-1">
{/* Timeline dot */} <div className="flex justify-between">
<div className={cn( <p className="font-medium">
"absolute -left-[31px] w-4 h-4 rounded-full border-2 border-background", {cambio.descripcion}
`bg-current ${config.color}` </p>
)} /> {/* BOTÓN PARA VER CAMBIOS */}
<Button
<Card className="card-interactive"> variant="ghost"
<CardContent className="py-4"> size="sm"
<div className="flex items-start gap-4"> className="gap-2 text-blue-600 hover:bg-blue-50 hover:text-blue-700"
<div className={cn( onClick={() => openCompareModal(cambio)}
"p-2 rounded-lg bg-muted flex-shrink-0", >
config.color <Eye className="h-4 w-4" />
)}> Ver cambios
<Icon className="w-4 h-4" /> </Button>
<span className="text-muted-foreground text-xs">
{format(cambio.fecha, 'HH:mm')}
</span>
</div> </div>
<div className="flex-1 min-w-0"> <div className="mt-2 flex items-center gap-2">
<div className="flex items-start justify-between gap-2"> <Badge
<div> variant="outline"
<p className="font-medium text-foreground"> className="text-[10px]"
{cambio.descripcion} >
</p> {config.label}
<div className="flex items-center gap-2 mt-1"> </Badge>
<Badge variant="outline" className="text-xs"> <span className="text-muted-foreground text-xs italic">
{config.label} por {cambio.usuario}
</Badge> </span>
{cambio.detalles?.campo && (
<span className="text-xs text-muted-foreground">
Campo: {cambio.detalles.campo}
</span>
)}
</div>
</div>
<span className="text-xs text-muted-foreground whitespace-nowrap">
{format(cambio.fecha, 'HH:mm')}
</span>
</div>
<div className="flex items-center gap-2 mt-3 text-xs text-muted-foreground">
<User className="w-3 h-3" />
<span>{cambio.usuario}</span>
<span className="text-muted-foreground/50"></span>
<span>
{formatDistanceToNow(cambio.fecha, { addSuffix: true, locale: es })}
</span>
</div>
</div> </div>
</div> </div>
</CardContent> </div>
</Card> </CardContent>
</div> </Card>
); </div>
})} )
</div> })}
</div> </div>
); </div>
})} ))}
</div> </div>
)} )}
{/* MODAL DE COMPARACIÓN */}
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="flex max-h-[90vh] max-w-4xl flex-col overflow-hidden">
<DialogHeader className="flex-shrink-0">
<DialogTitle className="flex items-center gap-2 text-xl">
<History className="h-5 w-5 text-blue-500" />
Comparación de cambios
</DialogTitle>
{/* ... info de usuario y fecha */}
</DialogHeader>
<div className="custom-scrollbar mt-4 flex-1 overflow-y-auto pr-2">
<div className="grid h-full grid-cols-2 gap-6">
{/* Lado Antes */}
<div className="flex flex-col space-y-3">
<div className="sticky top-0 z-10 flex items-center gap-2 bg-white pb-2">
<div className="h-2 w-2 rounded-full bg-red-400" />
<span className="text-xs font-bold text-slate-500 uppercase">
Versión Anterior
</span>
</div>
<div className="flex-1 rounded-xl border border-red-100 bg-red-50/30 p-4">
<RenderValue
value={selectedChange?.detalles.valor_anterior}
/>
</div>
</div>
{/* Lado Después */}
<div className="flex flex-col space-y-3">
<div className="sticky top-0 z-10 flex items-center gap-2 bg-white pb-2">
<div className="h-2 w-2 rounded-full bg-emerald-400" />
<span className="text-xs font-bold text-slate-500 uppercase">
Nueva Versión
</span>
</div>
<div className="flex-1 rounded-xl border border-emerald-100 bg-emerald-50/40 p-4">
<RenderValue value={selectedChange?.detalles.valor_nuevo} />
</div>
</div>
</div>
</div>
<div className="mt-4 flex flex-shrink-0 items-center justify-center gap-2 rounded-lg border border-slate-100 bg-slate-50 p-3 text-xs text-slate-500">
Campo modificado:{' '}
<Badge variant="secondary">{selectedChange?.detalles.campo}</Badge>
</div>
</DialogContent>
</Dialog>
</div> </div>
); )
} }

View File

@@ -1,161 +1,260 @@
import { Link, useRouterState } from '@tanstack/react-router'
import { ArrowLeft, GraduationCap, Pencil, Sparkles } from 'lucide-react'
import { useCallback, useState, useEffect } from 'react'
import { BibliographyItem } from './BibliographyItem'
import { ContenidoTematico } from './ContenidoTematico'
import { DocumentoSEPTab } from './DocumentoSEPTab'
import { HistorialTab } from './HistorialTab'
import { IAMateriaTab } from './IAMateriaTab'
import type { CampoEstructura, IAMessage, IASugerencia } from '@/types/materia'
import { useCallback, useState } from 'react'
import { Link } from '@tanstack/react-router'
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { import { useSubject } from '@/data/hooks/useSubjects'
ArrowLeft,
GraduationCap,
Edit2, Save,
Pencil
} from 'lucide-react'
import { ContenidoTematico } from './ContenidoTematico'
import { BibliographyItem } from './BibliographyItem'
import { IAMateriaTab } from './IAMateriaTab'
import type {
CampoEstructura,
IAMessage,
IASugerencia,
UnidadTematica,
} from '@/types/materia';
import { import {
mockMateria, mockMateria,
mockEstructura, mockEstructura,
mockDocumentoSep, mockDocumentoSep,
mockHistorial } from '@/data/mockMateriaData'
} from '@/data/mockMateriaData';
import { DocumentoSEPTab } from './DocumentoSEPTab'
import { HistorialTab } from './HistorialTab'
export interface BibliografiaEntry { export interface BibliografiaEntry {
id: string; id: string
tipo: 'BASICA' | 'COMPLEMENTARIA'; tipo: 'BASICA' | 'COMPLEMENTARIA'
cita: string; cita: string
fuenteBibliotecaId?: string; fuenteBibliotecaId?: string
fuenteBiblioteca?: any; fuenteBiblioteca?: any
} }
export interface BibliografiaTabProps { export interface BibliografiaTabProps {
bibliografia: BibliografiaEntry[]; bibliografia: Array<BibliografiaEntry>
onSave: (bibliografia: BibliografiaEntry[]) => void; onSave: (bibliografia: Array<BibliografiaEntry>) => void
isSaving: boolean; isSaving: boolean
} }
export interface AsignaturaDatos {
[key: string]: string
}
export interface AsignaturaResponse {
datos: AsignaturaDatos
}
function EditableHeaderField({
value,
onSave,
className,
}: {
value: string | number
onSave: (val: string) => void
className?: string
}) {
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault()
;(e.currentTarget as HTMLElement).blur() // Quita el foco
}
}
const handleBlur = (e: React.FocusEvent<HTMLElement>) => {
const newValue = e.currentTarget.textContent || ''
if (newValue !== value.toString()) {
onSave(newValue)
}
}
return (
<span
contentEditable
suppressContentEditableWarning
onKeyDown={handleKeyDown}
onBlur={handleBlur}
className={`cursor-text rounded px-1 transition-all outline-none focus:ring-2 focus:ring-blue-400 ${className}`}
>
{value}
</span>
)
}
export default function MateriaDetailPage() { export default function MateriaDetailPage() {
const routerState = useRouterState()
const state = routerState.location.state as any
const { data: asignaturasApi, isLoading: loadingAsig } = useSubject(
state?.realId,
)
// 1. Asegúrate de tener estos estados en tu componente principal // 1. Asegúrate de tener estos estados en tu componente principal
const [messages, setMessages] = useState<IAMessage[]>([]); const [messages, setMessages] = useState<Array<IAMessage>>([])
const [datosGenerales, setDatosGenerales] = useState({}); const [datosGenerales, setDatosGenerales] = useState({})
const [campos, setCampos] = useState<CampoEstructura[]>([]); const [campos, setCampos] = useState<Array<CampoEstructura>>([])
// 2. Funciones de manejo para la IA // Dentro de MateriaDetailPage
const handleSendMessage = (text: string, campoId?: string) => { const [headerData, setHeaderData] = useState({
const newMessage: IAMessage = { codigo: '',
id: Date.now().toString(), nombre: '',
role: 'user', creditos: 0,
content: text, ciclo: 0,
timestamp: new Date(), })
campoAfectado: campoId
};
setMessages([...messages, newMessage]);
// Aquí llamarías a tu API de OpenAI/Claude
//toast.info("Enviando consulta a la IA...");
};
const handleAcceptSuggestion = (sugerencia: IASugerencia) => { // Sincronizar cuando llegue la API
// Lógica para actualizar el valor del campo en tu estado de datosGenerales useEffect(() => {
//toast.success(`Sugerencia aplicada a ${sugerencia.campoNombre}`); if (asignaturasApi) {
}; setHeaderData({
codigo: asignaturasApi?.codigo ?? '',
nombre: asignaturasApi?.nombre ?? '',
creditos: asignaturasApi?.creditos ?? '',
ciclo: asignaturasApi?.numero_ciclo ?? 0,
})
}
}, [asignaturasApi])
const handleUpdateHeader = (key: string, value: string | number) => {
const newData = { ...headerData, [key]: value }
setHeaderData(newData)
console.log('💾 Guardando en estado y base de datos:', key, value)
}
/* ---------- sincronizar API ---------- */
useEffect(() => {
if (asignaturasApi?.datos) {
setDatosGenerales(asignaturasApi.datos)
}
}, [asignaturasApi])
// 2. Funciones de manejo para la IA
const handleSendMessage = (text: string, campoId?: string) => {
const newMessage: IAMessage = {
id: Date.now().toString(),
role: 'user',
content: text,
timestamp: new Date(),
campoAfectado: campoId,
}
setMessages([...messages, newMessage])
// Aquí llamarías a tu API de OpenAI/Claude
// toast.info("Enviando consulta a la IA...");
}
const handleAcceptSuggestion = (sugerencia: IASugerencia) => {
// Lógica para actualizar el valor del campo en tu estado de datosGenerales
// toast.success(`Sugerencia aplicada a ${sugerencia.campoNombre}`);
}
// Dentro de tu componente principal (donde están los Tabs) // Dentro de tu componente principal (donde están los Tabs)
const [bibliografia, setBibliografia] = useState<BibliografiaEntry[]>([ const [bibliografia, setBibliografia] = useState<Array<BibliografiaEntry>>([
{ {
id: '1', id: '1',
tipo: 'BASICA', tipo: 'BASICA',
cita: 'Russell, S., & Norvig, P. (2020). Artificial Intelligence: A Modern Approach. Pearson.' cita: 'Russell, S., & Norvig, P. (2020). Artificial Intelligence: A Modern Approach. Pearson.',
} },
]); ])
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false)
const handleSaveBibliografia = (data: BibliografiaEntry[]) => { const handleSaveBibliografia = (data: Array<BibliografiaEntry>) => {
setIsSaving(true); setIsSaving(true)
// Aquí iría tu llamada a la API // Aquí iría tu llamada a la API
setBibliografia(data); setBibliografia(data)
// Simulamos un guardado
setTimeout(() => {
setIsSaving(false);
//toast.success("Cambios guardados");
}, 1000);
};
const [isRegenerating, setIsRegenerating] = useState(false); // Simulamos un guardado
const handleRegenerateDocument = useCallback(() => {
setIsRegenerating(true);
setTimeout(() => { setTimeout(() => {
setIsRegenerating(false); setIsSaving(false)
}, 2000); // toast.success("Cambios guardados");
}, []); }, 1000)
}
const [isRegenerating, setIsRegenerating] = useState(false)
const handleRegenerateDocument = useCallback(() => {
setIsRegenerating(true)
setTimeout(() => {
setIsRegenerating(false)
}, 2000)
}, [])
return ( return (
<div className="w-full"> <div className="w-full">
{/* ================= HEADER ================= */} {/* ================= HEADER ACTUALIZADO ================= */}
<section className="bg-gradient-to-b from-[#0b1d3a] to-[#0e2a5c] text-white"> <section className="bg-gradient-to-b from-[#0b1d3a] to-[#0e2a5c] text-white">
<div className="max-w-7xl mx-auto px-6 py-10"> <div className="mx-auto max-w-7xl px-6 py-10">
<Link <Link
to="/planes" to="/planes"
className="flex items-center gap-2 text-sm text-blue-200 hover:text-white mb-4" className="mb-4 flex items-center gap-2 text-sm text-blue-200 hover:text-white"
> >
<ArrowLeft className="w-4 h-4" /> <ArrowLeft className="h-4 w-4" /> Volver al plan
Volver al plan
</Link> </Link>
<div className="flex items-start justify-between gap-6"> <div className="flex items-start justify-between gap-6">
<div className="space-y-3"> <div className="space-y-3">
<Badge className="bg-blue-900/50 border border-blue-700"> {/* CÓDIGO EDITABLE */}
IA-401 <Badge className="border border-blue-700 bg-blue-900/50">
<EditableHeaderField
value={headerData.codigo}
onSave={(val) => handleUpdateHeader('codigo', val)}
/>
</Badge> </Badge>
{/* NOMBRE EDITABLE */}
<h1 className="text-3xl font-bold"> <h1 className="text-3xl font-bold">
Inteligencia Artificial Aplicada <EditableHeaderField
value={headerData.nombre}
onSave={(val) => handleUpdateHeader('nombre', val)}
/>
</h1> </h1>
<div className="flex flex-wrap gap-4 text-sm text-blue-200"> <div className="flex flex-wrap gap-4 text-sm text-blue-200">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<GraduationCap className="w-4 h-4" /> <GraduationCap className="h-4 w-4" />
Ingeniería en Sistemas Computacionales {asignaturasApi?.planes_estudio?.datos?.nombre}
</span>
<span>
{asignaturasApi?.planes_estudio?.carreras?.facultades?.nombre}
</span> </span>
<span>Facultad de Ingeniería</span>
</div> </div>
<p className="text-sm text-blue-300"> <p className="text-sm text-blue-300">
Pertenece al plan:{' '} Pertenece al plan:{' '}
<span className="underline cursor-pointer"> <span className="cursor-pointer underline">
Licenciatura en Ingeniería en Sistemas Computacionales 2024 {asignaturasApi?.planes_estudio?.nombre}
</span> </span>
</p> </p>
</div> </div>
<div className="flex flex-col gap-2 items-end"> <div className="flex flex-col items-end gap-2 text-right">
<Badge variant="secondary">8 créditos</Badge> {/* CRÉDITOS EDITABLES */}
<Badge variant="secondary">7° semestre</Badge> <Badge variant="secondary" className="gap-1">
<Badge variant="secondary">Sistemas Inteligentes</Badge> <EditableHeaderField
value={headerData.creditos}
onSave={(val) =>
handleUpdateHeader('creditos', parseInt(val) || 0)
}
/>
<span>créditos</span>
</Badge>
{/* SEMESTRE EDITABLE */}
<Badge variant="secondary" className="gap-1">
<EditableHeaderField
value={headerData.ciclo}
onSave={(val) =>
handleUpdateHeader('ciclo', parseInt(val) || 0)
}
/>
<span>° ciclo</span>
</Badge>
<Badge variant="secondary">{asignaturasApi?.tipo}</Badge>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
{/* ================= TABS ================= */} {/* ================= TABS ================= */}
<section className="bg-white border-b"> <section className="border-b bg-white">
<div className="max-w-7xl mx-auto px-6"> <div className="mx-auto max-w-7xl px-6">
<Tabs defaultValue="datos"> <Tabs defaultValue="datos">
<TabsList className="h-auto bg-transparent p-0 gap-6"> <TabsList className="h-auto gap-6 bg-transparent p-0">
<TabsTrigger value="datos">Datos generales</TabsTrigger> <TabsTrigger value="datos">Datos generales</TabsTrigger>
<TabsTrigger value="contenido">Contenido temático</TabsTrigger> <TabsTrigger value="contenido">Contenido temático</TabsTrigger>
<TabsTrigger value="bibliografia">Bibliografía</TabsTrigger> <TabsTrigger value="bibliografia">Bibliografía</TabsTrigger>
@@ -168,30 +267,38 @@ const handleRegenerateDocument = useCallback(() => {
{/* ================= TAB: DATOS GENERALES ================= */} {/* ================= TAB: DATOS GENERALES ================= */}
<TabsContent value="datos"> <TabsContent value="datos">
<DatosGenerales /> <DatosGenerales data={datosGenerales} isLoading={loadingAsig} />
</TabsContent> </TabsContent>
<TabsContent value="contenido"> <TabsContent value="contenido">
<ContenidoTematico></ContenidoTematico> <ContenidoTematico
data={asignaturasApi}
isLoading={loadingAsig}
></ContenidoTematico>
</TabsContent> </TabsContent>
<TabsContent value="bibliografia"> <TabsContent value="bibliografia">
<BibliographyItem <BibliographyItem
bibliografia={bibliografia} bibliografia={bibliografia}
onSave={handleSaveBibliografia} onSave={handleSaveBibliografia}
isSaving={isSaving} isSaving={isSaving}
/> />
</TabsContent> </TabsContent>
<TabsContent value="ia"> <TabsContent value="ia">
<IAMateriaTab <IAMateriaTab
campos={campos} campos={campos}
datosGenerales={datosGenerales} datosGenerales={datosGenerales}
messages={messages} messages={messages}
onSendMessage={handleSendMessage} onSendMessage={handleSendMessage}
onAcceptSuggestion={handleAcceptSuggestion} onAcceptSuggestion={handleAcceptSuggestion}
onRejectSuggestion={(id) => console.log("Rechazada") /*toast.error("Sugerencia rechazada")*/} onRejectSuggestion={
/> (id) =>
console.log(
'Rechazada',
) /* toast.error("Sugerencia rechazada")*/
}
/>
</TabsContent> </TabsContent>
<TabsContent value="sep"> <TabsContent value="sep">
@@ -206,7 +313,7 @@ const handleRegenerateDocument = useCallback(() => {
</TabsContent> </TabsContent>
<TabsContent value="historial"> <TabsContent value="historial">
<HistorialTab historial={mockHistorial} /> <HistorialTab />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
</div> </div>
@@ -216,81 +323,81 @@ const handleRegenerateDocument = useCallback(() => {
} }
/* ================= TAB CONTENT ================= */ /* ================= TAB CONTENT ================= */
interface DatosGeneralesProps {
data: AsignaturaDatos
isLoading: boolean
}
function DatosGenerales({ data, isLoading }: DatosGeneralesProps) {
const formatTitle = (key: string): string =>
key.replace(/_/g, ' ').replace(/\b\w/g, (l: string) => l.toUpperCase())
function DatosGenerales() {
return ( return (
<div className="max-w-7xl mx-auto py-8 px-4 space-y-8 animate-in fade-in duration-500"> <div className="animate-in fade-in mx-auto max-w-7xl space-y-8 px-4 py-8 duration-500">
{/* Encabezado de la Sección */} {/* Encabezado de la Sección */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b pb-6"> <div className="flex flex-col justify-between gap-4 border-b pb-6 md:flex-row md:items-center">
<div> <div>
<h2 className="text-2xl font-bold tracking-tight text-slate-900">Datos Generales</h2> <h2 className="text-2xl font-bold tracking-tight text-slate-900">
<p className="text-slate-500 mt-1"> Datos Generales
</h2>
<p className="mt-1 text-slate-500">
Información oficial estructurada bajo los lineamientos de la SEP. Información oficial estructurada bajo los lineamientos de la SEP.
</p> </p>
</div> </div>
<div className="flex gap-3">
<Button variant="outline" size="sm" className="gap-2">
<Edit2 className="w-4 h-4" /> Editar borrador
</Button>
<Button size="sm" className="gap-2 bg-blue-600 hover:bg-blue-700">
<Save className="w-4 h-4" /> Guardar cambios
</Button>
</div>
</div> </div>
{/* Grid de Información */} {/* Grid de Información */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 gap-6 md:grid-cols-3">
{/* Columna Principal (Más ancha) */} {/* Columna Principal (Más ancha) */}
<div className="md:col-span-2 space-y-6"> <div className="space-y-6 md:col-span-2">
<div className="md:col-span-2 space-y-6"> {isLoading && <p>Cargando información...</p>}
<InfoCard
title="Competencias a Desarrollar" {!isLoading &&
subtitle="Competencias profesionales que se desarrollarán" Object.entries(data).map(([key, value]) => (
isList={true} <InfoCard
initialContent={`• Diseñar algoritmos de machine learning para clasificación y predicción\n• Implementar redes neuronales profundas para procesamiento de imágenes\n• Evaluar y optimizar modelos de IA considerando métricas`} key={key}
/> title={formatTitle(key)}
initialContent={value}
<InfoCard onEnhanceAI={(contenido) => {
title="Objetivo General" console.log('Llevar a IA:', contenido)
initialContent="Formar profesionales capaces de diseñar, implementar y evaluar sistemas de inteligencia artificial que resuelvan problemas complejos." // Aquí tu lógica: setPestañaActiva('mejorar-con-ia');
/> }}
</div> />
))}
<div className="space-y-6">
<InfoCard
title="Justificación"
initialContent="La inteligencia artificial es una de las tecnologías más disruptivas..."
/>
</div>
</div> </div>
{/* Columna Lateral (Información Secundaria) */} {/* Columna Lateral (Información Secundaria) */}
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-6"> <div className="space-y-6">
{/* Tarjeta de Requisitos */} {/* Tarjeta de Requisitos */}
<InfoCard <InfoCard
title="Requisitos y Seriación" title="Requisitos y Seriación"
type="requirements" type="requirements"
initialContent={[ initialContent={[
{ type: "Pre-requisito", code: "PA-301", name: "Programación Avanzada" }, {
{ type: "Co-requisito", code: "MAT-201", name: "Matemáticas Discretas" } type: 'Pre-requisito',
]} code: 'PA-301',
/> name: 'Programación Avanzada',
},
{
type: 'Co-requisito',
code: 'MAT-201',
name: 'Matemáticas Discretas',
},
]}
/>
{/* Tarjeta de Evaluación */} {/* Tarjeta de Evaluación */}
<InfoCard <InfoCard
title="Sistema de Evaluación" title="Sistema de Evaluación"
type="evaluation" type="evaluation"
initialContent={[ initialContent={[
{ label: "Exámenes parciales", value: "30%" }, { label: 'Exámenes parciales', value: '30%' },
{ label: "Proyecto integrador", value: "35%" }, { label: 'Proyecto integrador', value: '35%' },
{ label: "Prácticas de laboratorio", value: "20%" }, { label: 'Prácticas de laboratorio', value: '20%' },
{ label: "Participación", value: "15%" }, { label: 'Participación', value: '15%' },
]} ]}
/> />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@@ -298,51 +405,85 @@ function DatosGenerales() {
} }
interface InfoCardProps { interface InfoCardProps {
title: string, title: string
subtitle?: string initialContent: any
isList?:boolean type?: 'text' | 'requirements' | 'evaluation'
initialContent: any // Puede ser string o array de objetos onEnhanceAI?: (content: any) => void // Nueva prop para la acción de IA
type?: 'text' | 'list' | 'requirements' | 'evaluation'
} }
function InfoCard({ title, initialContent, type = 'text' }: InfoCardProps) { function InfoCard({
title,
initialContent,
type = 'text',
onEnhanceAI,
}: InfoCardProps) {
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const [data, setData] = useState(initialContent) const [data, setData] = useState(initialContent)
// Estado temporal para el área de texto (siempre editamos como texto por simplicidad)
const [tempText, setTempText] = useState( const [tempText, setTempText] = useState(
type === 'text' || type === 'list' type === 'text' ? initialContent : JSON.stringify(initialContent, null, 2),
? initialContent
: JSON.stringify(initialContent, null, 2) // O un formato legible
) )
const handleSave = () => { const handleSave = () => {
// Aquí podrías parsear el texto de vuelta si es necesario setData(tempText)
setData(tempText)
setIsEditing(false) setIsEditing(false)
} }
return ( return (
<Card className="transition-all hover:border-slate-300"> <Card className="transition-all hover:border-slate-300">
<CardHeader className="pb-3 flex flex-row items-start justify-between space-y-0"> <CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3">
<CardTitle className="text-sm font-bold text-slate-700">{title}</CardTitle> <CardTitle className="text-sm font-bold text-slate-700">
{title}
</CardTitle>
{!isEditing && ( {!isEditing && (
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400" onClick={() => setIsEditing(true)}> <div className="flex gap-1">
<Pencil className="h-3 w-3" /> {/* NUEVO: Botón de Mejorar con IA */}
</Button> <Button
variant="ghost"
size="icon"
className="h-8 w-8 text-blue-500 hover:bg-blue-50 hover:text-blue-600"
onClick={() => onEnhanceAI?.(data)} // Enviamos la data actual a la IA
title="Mejorar con IA"
>
<Sparkles className="h-4 w-4" />
</Button>
{/* Botón de Editar original */}
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400"
onClick={() => setIsEditing(true)}
>
<Pencil className="h-3 w-3" />
</Button>
</div>
)} )}
</CardHeader> </CardHeader>
<CardContent> <CardContent>
{isEditing ? ( {isEditing ? (
<div className="space-y-3"> <div className="space-y-3">
<Textarea <Textarea
value={tempText} value={tempText}
onChange={(e) => setTempText(e.target.value)} onChange={(e) => setTempText(e.target.value)}
className="text-xs min-h-[100px]" className="min-h-[100px] text-xs"
/> />
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button size="sm" variant="ghost" onClick={() => setIsEditing(false)}>Cancelar</Button> <Button
<Button size="sm" className="bg-[#00a878]" onClick={handleSave}>Guardar</Button> size="sm"
variant="ghost"
onClick={() => setIsEditing(false)}
>
Cancelar
</Button>
<Button
size="sm"
className="bg-[#00a878] hover:bg-[#008f66]"
onClick={handleSave}
>
Guardar
</Button>
</div> </div>
</div> </div>
) : ( ) : (
@@ -358,13 +499,20 @@ function InfoCard({ title, initialContent, type = 'text' }: InfoCardProps) {
} }
// Vista de Requisitos // Vista de Requisitos
function RequirementsView({ items }: { items: any[] }) { function RequirementsView({ items }: { items: Array<any> }) {
return ( return (
<div className="space-y-3"> <div className="space-y-3">
{items.map((req, i) => ( {items.map((req, i) => (
<div key={i} className="p-3 bg-slate-50 rounded-lg border border-slate-100"> <div
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-tight">{req.type}</p> key={i}
<p className="text-sm font-medium text-slate-700">{req.code} {req.name}</p> className="rounded-lg border border-slate-100 bg-slate-50 p-3"
>
<p className="text-[10px] font-bold tracking-tight text-slate-400 uppercase">
{req.type}
</p>
<p className="text-sm font-medium text-slate-700">
{req.code} {req.name}
</p>
</div> </div>
))} ))}
</div> </div>
@@ -372,11 +520,14 @@ function RequirementsView({ items }: { items: any[] }) {
} }
// Vista de Evaluación // Vista de Evaluación
function EvaluationView({ items }: { items: any[] }) { function EvaluationView({ items }: { items: Array<any> }) {
return ( return (
<div className="space-y-2"> <div className="space-y-2">
{items.map((item, i) => ( {items.map((item, i) => (
<div key={i} className="flex justify-between text-sm border-b border-slate-50 pb-1.5 italic"> <div
key={i}
className="flex justify-between border-b border-slate-50 pb-1.5 text-sm italic"
>
<span className="text-slate-500">{item.label}</span> <span className="text-slate-500">{item.label}</span>
<span className="font-bold text-blue-600">{item.value}</span> <span className="font-bold text-blue-600">{item.value}</span>
</div> </div>
@@ -384,13 +535,3 @@ function EvaluationView({ items }: { items: any[] }) {
</div> </div>
) )
} }
function EmptyTab({ title }: { title: string }) {
return (
<div className="py-16 text-center text-muted-foreground">
{title} (pendiente)
</div>
)
}

View File

@@ -1,7 +1,7 @@
import { supabaseBrowser } from "../supabase/client"; import { supabaseBrowser } from '../supabase/client'
import { invokeEdge } from "../supabase/invokeEdge"; import { invokeEdge } from '../supabase/invokeEdge'
import { buildRange, requireData, throwIfError } from "./_helpers"; import { buildRange, requireData, throwIfError } from './_helpers'
import type { import type {
Asignatura, Asignatura,
@@ -13,60 +13,59 @@ import type {
PlanEstudio, PlanEstudio,
TipoCiclo, TipoCiclo,
UUID, UUID,
} from "../types/domain"; } from '../types/domain'
import type { UploadedFile } from "@/components/planes/wizard/PasoDetallesPanel/FileDropZone"; import type { UploadedFile } from '@/components/planes/wizard/PasoDetallesPanel/FileDropZone'
const EDGE = { const EDGE = {
plans_create_manual: "plans_create_manual", plans_create_manual: 'plans_create_manual',
ai_generate_plan: "ai-generate-plan", ai_generate_plan: 'ai-generate-plan',
plans_persist_from_ai: "plans_persist_from_ai", plans_persist_from_ai: 'plans_persist_from_ai',
plans_clone_from_existing: "plans_clone_from_existing", plans_clone_from_existing: 'plans_clone_from_existing',
plans_import_from_files: "plans_import_from_files", plans_import_from_files: 'plans_import_from_files',
plans_update_fields: "plans_update_fields", plans_update_fields: 'plans_update_fields',
plans_update_map: "plans_update_map", plans_update_map: 'plans_update_map',
plans_transition_state: "plans_transition_state", plans_transition_state: 'plans_transition_state',
plans_generate_document: "plans_generate_document", plans_generate_document: 'plans_generate_document',
plans_get_document: "plans_get_document", plans_get_document: 'plans_get_document',
} as const; } as const
export type PlanListFilters = { export type PlanListFilters = {
search?: string; search?: string
carreraId?: UUID; carreraId?: UUID
facultadId?: UUID; // filtra por carreras.facultad_id facultadId?: UUID // filtra por carreras.facultad_id
estadoId?: UUID; estadoId?: UUID
activo?: boolean; activo?: boolean
limit?: number; limit?: number
offset?: number; offset?: number
}; }
// Helper para limpiar texto (lo movemos fuera para reutilizar o lo dejas en un utils) // Helper para limpiar texto (lo movemos fuera para reutilizar o lo dejas en un utils)
const cleanText = (text: string) => { const cleanText = (text: string) => {
return text return text
.normalize("NFD") .normalize('NFD')
.replace(/[\u0300-\u036f]/g, "") .replace(/[\u0300-\u036f]/g, '')
.toLowerCase(); .toLowerCase()
}; }
export async function plans_list( export async function plans_list(
filters: PlanListFilters = {}, filters: PlanListFilters = {},
): Promise<Paged<PlanEstudio>> { ): Promise<Paged<PlanEstudio>> {
const supabase = supabaseBrowser(); const supabase = supabaseBrowser()
// 1. Construimos la query base // 1. Construimos la query base
// NOTA IMPORTANTE: Para filtrar planes basados en facultad (que está en carreras), // NOTA IMPORTANTE: Para filtrar planes basados en facultad (que está en carreras),
// necesitamos hacer un INNER JOIN. En Supabase se usa "!inner". // necesitamos hacer un INNER JOIN. En Supabase se usa "!inner".
// Si filters.facultadId existe, forzamos el inner join, si no, lo dejamos normal. // Si filters.facultadId existe, forzamos el inner join, si no, lo dejamos normal.
const carreraModifier = filters.facultadId && filters.facultadId !== "todas" const carreraModifier =
? "!inner" filters.facultadId && filters.facultadId !== 'todas' ? '!inner' : ''
: "";
let q = supabase let q = supabase
.from("planes_estudio") .from('planes_estudio')
.select( .select(
` `
*, *,
@@ -77,56 +76,56 @@ export async function plans_list(
estructuras_plan (*), estructuras_plan (*),
estados_plan (*) estados_plan (*)
`, `,
{ count: "exact" }, { count: 'exact' },
) )
.order("actualizado_en", { ascending: false }); .order('actualizado_en', { ascending: false })
// 2. Aplicamos filtros dinámicos // 2. Aplicamos filtros dinámicos
// SOLUCIÓN SEARCH: Limpiamos el input y buscamos en la columna generada // SOLUCIÓN SEARCH: Limpiamos el input y buscamos en la columna generada
if (filters.search?.trim()) { if (filters.search?.trim()) {
const cleanTerm = cleanText(filters.search.trim()); const cleanTerm = cleanText(filters.search.trim())
// Usamos la columna nueva creada en el Paso 1 // Usamos la columna nueva creada en el Paso 1
q = q.ilike("nombre_search", `%${cleanTerm}%`); q = q.ilike('nombre_search', `%${cleanTerm}%`)
} }
if (filters.carreraId && filters.carreraId !== "todas") { if (filters.carreraId && filters.carreraId !== 'todas') {
q = q.eq("carrera_id", filters.carreraId); q = q.eq('carrera_id', filters.carreraId)
} }
if (filters.estadoId && filters.estadoId !== "todos") { if (filters.estadoId && filters.estadoId !== 'todos') {
q = q.eq("estado_actual_id", filters.estadoId); q = q.eq('estado_actual_id', filters.estadoId)
} }
if (typeof filters.activo === "boolean") { if (typeof filters.activo === 'boolean') {
q = q.eq("activo", filters.activo); q = q.eq('activo', filters.activo)
} }
// Filtro por facultad (gracias al !inner arriba, esto filtrará los planes) // Filtro por facultad (gracias al !inner arriba, esto filtrará los planes)
if (filters.facultadId && filters.facultadId !== "todas") { if (filters.facultadId && filters.facultadId !== 'todas') {
q = q.eq("carreras.facultad_id", filters.facultadId); q = q.eq('carreras.facultad_id', filters.facultadId)
} }
// 3. Paginación // 3. Paginación
const { from, to } = buildRange(filters.limit, filters.offset); const { from, to } = buildRange(filters.limit, filters.offset)
if (from !== undefined && to !== undefined) q = q.range(from, to); if (from !== undefined && to !== undefined) q = q.range(from, to)
const { data, error, count } = await q; const { data, error, count } = await q
throwIfError(error); throwIfError(error)
return { return {
// 1. Si data es null, usa []. // 1. Si data es null, usa [].
// 2. Luego dile a TS que el resultado es tu Array tipado. // 2. Luego dile a TS que el resultado es tu Array tipado.
data: (data ?? []) as unknown as Array<PlanEstudio>, data: (data ?? []) as unknown as Array<PlanEstudio>,
count: count ?? 0, count: count ?? 0,
}; }
} }
export async function plans_get(planId: UUID): Promise<PlanEstudio> { export async function plans_get(planId: UUID): Promise<PlanEstudio> {
const supabase = supabaseBrowser(); const supabase = supabaseBrowser()
const { data, error } = await supabase const { data, error } = await supabase
.from("planes_estudio") .from('planes_estudio')
.select( .select(
` `
*, *,
@@ -135,219 +134,217 @@ export async function plans_get(planId: UUID): Promise<PlanEstudio> {
estados_plan (*) estados_plan (*)
`, `,
) )
.eq("id", planId) .eq('id', planId)
.single(); .single()
throwIfError(error); throwIfError(error)
return requireData(data, "Plan no encontrado."); return requireData(data, 'Plan no encontrado.')
} }
export async function plan_lineas_list( export async function plan_lineas_list(
planId: UUID, planId: UUID,
): Promise<Array<LineaPlan>> { ): Promise<Array<LineaPlan>> {
const supabase = supabaseBrowser(); const supabase = supabaseBrowser()
const { data, error } = await supabase const { data, error } = await supabase
.from("lineas_plan") .from('lineas_plan')
.select("id,plan_estudio_id,nombre,orden,area,creado_en,actualizado_en") .select('id,plan_estudio_id,nombre,orden,area,creado_en,actualizado_en')
.eq("plan_estudio_id", planId) .eq('plan_estudio_id', planId)
.order("orden", { ascending: true }); .order('orden', { ascending: true })
throwIfError(error); throwIfError(error)
return data ?? []; return data ?? []
} }
export async function plan_asignaturas_list( export async function plan_asignaturas_list(
planId: UUID, planId: UUID,
): Promise<Array<Asignatura>> { ): Promise<Array<Asignatura>> {
const supabase = supabaseBrowser(); const supabase = supabaseBrowser()
const { data, error } = await supabase const { data, error } = await supabase
.from("asignaturas") .from('asignaturas')
.select( .select(
"id,plan_estudio_id,estructura_id,facultad_propietaria_id,codigo,nombre,tipo,creditos,horas_semana,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en", 'id,plan_estudio_id,estructura_id,facultad_propietaria_id,codigo,nombre,tipo,creditos,horas_semana,numero_ciclo,linea_plan_id,orden_celda,datos,contenido_tematico,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en',
) )
.eq("plan_estudio_id", planId) .eq('plan_estudio_id', planId)
.order("numero_ciclo", { ascending: true, nullsFirst: false }) .order('numero_ciclo', { ascending: true, nullsFirst: false })
.order("orden_celda", { ascending: true, nullsFirst: false }) .order('orden_celda', { ascending: true, nullsFirst: false })
.order("nombre", { ascending: true }); .order('nombre', { ascending: true })
throwIfError(error); throwIfError(error)
return data ?? []; return data ?? []
} }
export async function plans_history(planId: UUID): Promise<Array<CambioPlan>> { export async function plans_history(planId: UUID): Promise<Array<CambioPlan>> {
const supabase = supabaseBrowser(); const supabase = supabaseBrowser()
const { data, error } = await supabase const { data, error } = await supabase
.from("cambios_plan") .from('cambios_plan')
.select( .select(
"id,plan_estudio_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo,interaccion_ia_id", 'id,plan_estudio_id,cambiado_por,cambiado_en,tipo,campo,valor_anterior,valor_nuevo,interaccion_ia_id',
) )
.eq("plan_estudio_id", planId) .eq('plan_estudio_id', planId)
.order("cambiado_en", { ascending: false }); .order('cambiado_en', { ascending: false })
throwIfError(error); throwIfError(error)
return data ?? []; return data ?? []
} }
/** Wizard: crear plan manual (Edge Function) */ /** Wizard: crear plan manual (Edge Function) */
export type PlansCreateManualInput = { export type PlansCreateManualInput = {
carreraId: UUID; carreraId: UUID
estructuraId: UUID; estructuraId: UUID
nombre: string; nombre: string
nivel: NivelPlanEstudio; nivel: NivelPlanEstudio
tipoCiclo: TipoCiclo; tipoCiclo: TipoCiclo
numCiclos: number; numCiclos: number
datos?: Partial<PlanDatosSep> & Record<string, any>; datos?: Partial<PlanDatosSep> & Record<string, any>
}; }
export async function plans_create_manual( export async function plans_create_manual(
input: PlansCreateManualInput, input: PlansCreateManualInput,
): Promise<PlanEstudio> { ): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_create_manual, input); return invokeEdge<PlanEstudio>(EDGE.plans_create_manual, input)
} }
/** Wizard: IA genera preview JSON (Edge Function) */ /** Wizard: IA genera preview JSON (Edge Function) */
export type AIGeneratePlanInput = { export type AIGeneratePlanInput = {
datosBasicos: { datosBasicos: {
nombrePlan: string; nombrePlan: string
carreraId: UUID; carreraId: UUID
facultadId?: UUID; facultadId?: UUID
nivel: string; nivel: string
tipoCiclo: TipoCiclo; tipoCiclo: TipoCiclo
numCiclos: number; numCiclos: number
estructuraPlanId: UUID; estructuraPlanId: UUID
}; }
iaConfig: { iaConfig: {
descripcionEnfoque: string; descripcionEnfoque: string
notasAdicionales?: string; notasAdicionales?: string
archivosReferencia?: Array<UUID>; archivosReferencia?: Array<UUID>
repositoriosIds?: Array<UUID>; repositoriosIds?: Array<UUID>
archivosAdjuntos: Array<UploadedFile>; archivosAdjuntos: Array<UploadedFile>
usarMCP?: boolean; usarMCP?: boolean
}; }
}; }
export async function ai_generate_plan( export async function ai_generate_plan(
input: AIGeneratePlanInput, input: AIGeneratePlanInput,
): Promise<any> { ): Promise<any> {
console.log("input ai generate", input); console.log('input ai generate', input)
const edgeFunctionBody = new FormData(); const edgeFunctionBody = new FormData()
edgeFunctionBody.append("datosBasicos", JSON.stringify(input.datosBasicos)); edgeFunctionBody.append('datosBasicos', JSON.stringify(input.datosBasicos))
edgeFunctionBody.append( edgeFunctionBody.append(
"iaConfig", 'iaConfig',
JSON.stringify({ JSON.stringify({
...input.iaConfig, ...input.iaConfig,
archivosAdjuntos: undefined, // los manejamos aparte archivosAdjuntos: undefined, // los manejamos aparte
}), }),
); )
input.iaConfig.archivosAdjuntos.forEach((file, index) => { input.iaConfig.archivosAdjuntos.forEach((file, index) => {
edgeFunctionBody.append(`archivosAdjuntos`, file.file); edgeFunctionBody.append(`archivosAdjuntos`, file.file)
}); })
return invokeEdge<any>( return invokeEdge<any>(
EDGE.ai_generate_plan, EDGE.ai_generate_plan,
edgeFunctionBody, edgeFunctionBody,
undefined, undefined,
supabaseBrowser(), supabaseBrowser(),
); )
} }
export async function plans_persist_from_ai( export async function plans_persist_from_ai(payload: {
payload: { jsonPlan: any }, jsonPlan: any
): Promise<PlanEstudio> { }): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_persist_from_ai, payload); return invokeEdge<PlanEstudio>(EDGE.plans_persist_from_ai, payload)
} }
export async function plans_clone_from_existing(payload: { export async function plans_clone_from_existing(payload: {
planOrigenId: UUID; planOrigenId: UUID
overrides: overrides: Partial<
& Partial< Pick<PlanEstudio, 'nombre' | 'nivel' | 'tipo_ciclo' | 'numero_ciclos'>
Pick<PlanEstudio, "nombre" | "nivel" | "tipo_ciclo" | "numero_ciclos"> > & {
> carrera_id?: UUID
& { estructura_id?: UUID
carrera_id?: UUID; datos?: Partial<PlanDatosSep> & Record<string, any>
estructura_id?: UUID; }
datos?: Partial<PlanDatosSep> & Record<string, any>;
};
}): Promise<PlanEstudio> { }): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_clone_from_existing, payload); return invokeEdge<PlanEstudio>(EDGE.plans_clone_from_existing, payload)
} }
export async function plans_import_from_files(payload: { export async function plans_import_from_files(payload: {
datosBasicos: { datosBasicos: {
nombrePlan: string; nombrePlan: string
carreraId: UUID; carreraId: UUID
estructuraId: UUID; estructuraId: UUID
nivel: string; nivel: string
tipoCiclo: TipoCiclo; tipoCiclo: TipoCiclo
numCiclos: number; numCiclos: number
}; }
archivoWordPlanId: UUID; archivoWordPlanId: UUID
archivoMapaExcelId?: UUID | null; archivoMapaExcelId?: UUID | null
archivoMateriasExcelId?: UUID | null; archivoMateriasExcelId?: UUID | null
}): Promise<PlanEstudio> { }): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_import_from_files, payload); return invokeEdge<PlanEstudio>(EDGE.plans_import_from_files, payload)
} }
/** Update de tarjetas/fields del plan (Edge Function: merge server-side) */ /** Update de tarjetas/fields del plan (Edge Function: merge server-side) */
export type PlansUpdateFieldsPatch = { export type PlansUpdateFieldsPatch = {
nombre?: string; nombre?: string
nivel?: NivelPlanEstudio; nivel?: NivelPlanEstudio
tipo_ciclo?: TipoCiclo; tipo_ciclo?: TipoCiclo
numero_ciclos?: number; numero_ciclos?: number
datos?: Partial<PlanDatosSep> & Record<string, any>; datos?: Partial<PlanDatosSep> & Record<string, any>
}; }
export async function plans_update_fields( export async function plans_update_fields(
planId: UUID, planId: UUID,
patch: PlansUpdateFieldsPatch, patch: PlansUpdateFieldsPatch,
): Promise<PlanEstudio> { ): Promise<PlanEstudio> {
return invokeEdge<PlanEstudio>(EDGE.plans_update_fields, { planId, patch }); return invokeEdge<PlanEstudio>(EDGE.plans_update_fields, { planId, patch })
} }
/** Operaciones del mapa curricular (mover/reordenar) */ /** Operaciones del mapa curricular (mover/reordenar) */
export type PlanMapOperation = export type PlanMapOperation =
| { | {
op: "MOVE_ASIGNATURA"; op: 'MOVE_ASIGNATURA'
asignaturaId: UUID; asignaturaId: UUID
numero_ciclo: number | null; numero_ciclo: number | null
linea_plan_id: UUID | null; linea_plan_id: UUID | null
orden_celda?: number | null; orden_celda?: number | null
} }
| { | {
op: "REORDER_CELDA"; op: 'REORDER_CELDA'
linea_plan_id: UUID; linea_plan_id: UUID
numero_ciclo: number; numero_ciclo: number
asignaturaIdsOrdenados: Array<UUID>; asignaturaIdsOrdenados: Array<UUID>
}; }
export async function plans_update_map( export async function plans_update_map(
planId: UUID, planId: UUID,
ops: Array<PlanMapOperation>, ops: Array<PlanMapOperation>,
): Promise<{ ok: true }> { ): Promise<{ ok: true }> {
return invokeEdge<{ ok: true }>(EDGE.plans_update_map, { planId, ops }); return invokeEdge<{ ok: true }>(EDGE.plans_update_map, { planId, ops })
} }
export async function plans_transition_state(payload: { export async function plans_transition_state(payload: {
planId: UUID; planId: UUID
haciaEstadoId: UUID; haciaEstadoId: UUID
comentario?: string; comentario?: string
}): Promise<{ ok: true }> { }): Promise<{ ok: true }> {
return invokeEdge<{ ok: true }>(EDGE.plans_transition_state, payload); return invokeEdge<{ ok: true }>(EDGE.plans_transition_state, payload)
} }
/** Documento (Edge Function: genera y devuelve URL firmada o metadata) */ /** Documento (Edge Function: genera y devuelve URL firmada o metadata) */
export type DocumentoResult = { export type DocumentoResult = {
archivoId: UUID; archivoId: UUID
signedUrl: string; signedUrl: string
mimeType?: string; mimeType?: string
nombre?: string; nombre?: string
}; }
export async function plans_generate_document( export async function plans_generate_document(
planId: UUID, planId: UUID,
): Promise<DocumentoResult> { ): Promise<DocumentoResult> {
return invokeEdge<DocumentoResult>(EDGE.plans_generate_document, { planId }); return invokeEdge<DocumentoResult>(EDGE.plans_generate_document, { planId })
} }
export async function plans_get_document( export async function plans_get_document(
@@ -355,26 +352,26 @@ export async function plans_get_document(
): Promise<DocumentoResult | null> { ): Promise<DocumentoResult | null> {
return invokeEdge<DocumentoResult | null>(EDGE.plans_get_document, { return invokeEdge<DocumentoResult | null>(EDGE.plans_get_document, {
planId, planId,
}); })
} }
export async function getCatalogos() { export async function getCatalogos() {
const supabase = supabaseBrowser(); const supabase = supabaseBrowser()
const [facultadesRes, carrerasRes, estadosRes, estructurasPlanRes] = const [facultadesRes, carrerasRes, estadosRes, estructurasPlanRes] =
await Promise.all([ await Promise.all([
supabase.from("facultades").select("*").order("nombre"), supabase.from('facultades').select('*').order('nombre'),
supabase.from("carreras").select("*").order("nombre"), supabase.from('carreras').select('*').order('nombre'),
supabase.from("estados_plan").select("*").order("orden"), supabase.from('estados_plan').select('*').order('orden'),
supabase.from("estructuras_plan").select("*").order("creado_en", { supabase.from('estructuras_plan').select('*').order('creado_en', {
ascending: true, ascending: true,
}), }),
]); ])
return { return {
facultades: facultadesRes.data ?? [], facultades: facultadesRes.data ?? [],
carreras: carrerasRes.data ?? [], carreras: carrerasRes.data ?? [],
estados: estadosRes.data ?? [], estados: estadosRes.data ?? [],
estructurasPlan: estructurasPlanRes.data ?? [], estructurasPlan: estructurasPlanRes.data ?? [],
}; }
} }

View File

@@ -3,7 +3,7 @@ import {
useMutation, useMutation,
useQuery, useQuery,
useQueryClient, useQueryClient,
} from "@tanstack/react-query"; } from '@tanstack/react-query'
import { import {
ai_generate_plan, ai_generate_plan,
@@ -22,16 +22,16 @@ import {
plans_transition_state, plans_transition_state,
plans_update_fields, plans_update_fields,
plans_update_map, plans_update_map,
} from "../api/plans.api"; } from '../api/plans.api'
import { qk } from "../query/keys"; import { qk } from '../query/keys'
import type { import type {
PlanListFilters, PlanListFilters,
PlanMapOperation, PlanMapOperation,
PlansCreateManualInput, PlansCreateManualInput,
PlansUpdateFieldsPatch, PlansUpdateFieldsPatch,
} from "../api/plans.api"; } from '../api/plans.api'
import type { UUID } from "../types/domain"; import type { UUID } from '../types/domain'
export function usePlanes(filters: PlanListFilters) { export function usePlanes(filters: PlanListFilters) {
// 🧠 Tip: memoiza "filters" (useMemo) para que queryKey sea estable. // 🧠 Tip: memoiza "filters" (useMemo) para que queryKey sea estable.
@@ -47,146 +47,146 @@ export function usePlanes(filters: PlanListFilters) {
// Opcional: Tiempo que la data se considera fresca // Opcional: Tiempo que la data se considera fresca
staleTime: 1000 * 60 * 5, // 5 minutos staleTime: 1000 * 60 * 5, // 5 minutos
}); })
} }
export function usePlan(planId: UUID | null | undefined) { export function usePlan(planId: UUID | null | undefined) {
return useQuery({ return useQuery({
queryKey: planId ? qk.plan(planId) : ["planes", "detail", null], queryKey: planId ? qk.plan(planId) : ['planes', 'detail', null],
queryFn: () => plans_get(planId as UUID), queryFn: () => plans_get(planId as UUID),
enabled: Boolean(planId), enabled: Boolean(planId),
}); })
} }
export function usePlanLineas(planId: UUID | null | undefined) { export function usePlanLineas(planId: UUID | null | undefined) {
return useQuery({ return useQuery({
queryKey: planId ? qk.planLineas(planId) : ["planes", "lineas", null], queryKey: planId ? qk.planLineas(planId) : ['planes', 'lineas', null],
queryFn: () => plan_lineas_list(planId as UUID), queryFn: () => plan_lineas_list(planId as UUID),
enabled: Boolean(planId), enabled: Boolean(planId),
}); })
} }
export function usePlanAsignaturas(planId: UUID | null | undefined) { export function usePlanAsignaturas(planId: UUID | null | undefined) {
return useQuery({ return useQuery({
queryKey: planId queryKey: planId
? qk.planAsignaturas(planId) ? qk.planAsignaturas(planId)
: ["planes", "asignaturas", null], : ['planes', 'asignaturas', null],
queryFn: () => plan_asignaturas_list(planId as UUID), queryFn: () => plan_asignaturas_list(planId as UUID),
enabled: Boolean(planId), enabled: Boolean(planId),
}); })
} }
export function usePlanHistorial(planId: UUID | null | undefined) { export function usePlanHistorial(planId: UUID | null | undefined) {
return useQuery({ return useQuery({
queryKey: planId ? qk.planHistorial(planId) : ["planes", "historial", null], queryKey: planId ? qk.planHistorial(planId) : ['planes', 'historial', null],
queryFn: () => plans_history(planId as UUID), queryFn: () => plans_history(planId as UUID),
enabled: Boolean(planId), enabled: Boolean(planId),
}); })
} }
export function usePlanDocumento(planId: UUID | null | undefined) { export function usePlanDocumento(planId: UUID | null | undefined) {
return useQuery({ return useQuery({
queryKey: planId ? qk.planDocumento(planId) : ["planes", "documento", null], queryKey: planId ? qk.planDocumento(planId) : ['planes', 'documento', null],
queryFn: () => plans_get_document(planId as UUID), queryFn: () => plans_get_document(planId as UUID),
enabled: Boolean(planId), enabled: Boolean(planId),
staleTime: 30_000, staleTime: 30_000,
}); })
} }
export function useCatalogosPlanes() { export function useCatalogosPlanes() {
return useQuery({ return useQuery({
queryKey: ["catalogos_planes"], queryKey: ['catalogos_planes'],
queryFn: getCatalogos, queryFn: getCatalogos,
staleTime: 1000 * 60 * 60, // 1 hora de caché (estos datos casi no cambian) staleTime: 1000 * 60 * 60, // 1 hora de caché (estos datos casi no cambian)
}); })
} }
/* ------------------ Mutations ------------------ */ /* ------------------ Mutations ------------------ */
export function useCreatePlanManual() { export function useCreatePlanManual() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (input: PlansCreateManualInput) => plans_create_manual(input), mutationFn: (input: PlansCreateManualInput) => plans_create_manual(input),
onSuccess: (plan) => { onSuccess: (plan) => {
qc.invalidateQueries({ queryKey: ["planes", "list"] }); qc.invalidateQueries({ queryKey: ['planes', 'list'] })
qc.setQueryData(qk.plan(plan.id), plan); qc.setQueryData(qk.plan(plan.id), plan)
}, },
}); })
} }
export function useGeneratePlanAI() { export function useGeneratePlanAI() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: ai_generate_plan, mutationFn: ai_generate_plan,
onSuccess: (data) => { onSuccess: (data) => {
// Asumiendo que la Edge Function devuelve { ok: true, plan: { id: ... } } // Asumiendo que la Edge Function devuelve { ok: true, plan: { id: ... } }
const newPlan = data.plan; const newPlan = data.plan
if (newPlan) { if (newPlan) {
// 1. Invalidar la lista para que aparezca el nuevo plan // 1. Invalidar la lista para que aparezca el nuevo plan
qc.invalidateQueries({ queryKey: ["planes", "list"] }); qc.invalidateQueries({ queryKey: ['planes', 'list'] })
// 2. (Opcional) Pre-cargar el dato individual para que la navegación sea instantánea // 2. (Opcional) Pre-cargar el dato individual para que la navegación sea instantánea
// qc.setQueryData(["planes", "detail", newPlan.id], newPlan); // qc.setQueryData(["planes", "detail", newPlan.id], newPlan);
} }
}, },
}); })
} }
// Funcion obsoleta porque ahora el plan se persiste directamente en useGeneratePlanAI // Funcion obsoleta porque ahora el plan se persiste directamente en useGeneratePlanAI
export function usePersistPlanFromAI() { export function usePersistPlanFromAI() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (payload: { jsonPlan: any }) => plans_persist_from_ai(payload), mutationFn: (payload: { jsonPlan: any }) => plans_persist_from_ai(payload),
onSuccess: (plan) => { onSuccess: (plan) => {
qc.invalidateQueries({ queryKey: ["planes", "list"] }); qc.invalidateQueries({ queryKey: ['planes', 'list'] })
qc.setQueryData(qk.plan(plan.id), plan); qc.setQueryData(qk.plan(plan.id), plan)
}, },
}); })
} }
export function useClonePlan() { export function useClonePlan() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: plans_clone_from_existing, mutationFn: plans_clone_from_existing,
onSuccess: (plan) => { onSuccess: (plan) => {
qc.invalidateQueries({ queryKey: ["planes", "list"] }); qc.invalidateQueries({ queryKey: ['planes', 'list'] })
qc.setQueryData(qk.plan(plan.id), plan); qc.setQueryData(qk.plan(plan.id), plan)
}, },
}); })
} }
export function useImportPlanFromFiles() { export function useImportPlanFromFiles() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: plans_import_from_files, mutationFn: plans_import_from_files,
onSuccess: (plan) => { onSuccess: (plan) => {
qc.invalidateQueries({ queryKey: ["planes", "list"] }); qc.invalidateQueries({ queryKey: ['planes', 'list'] })
qc.setQueryData(qk.plan(plan.id), plan); qc.setQueryData(qk.plan(plan.id), plan)
}, },
}); })
} }
export function useUpdatePlanFields() { export function useUpdatePlanFields() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (vars: { planId: UUID; patch: PlansUpdateFieldsPatch }) => mutationFn: (vars: { planId: UUID; patch: PlansUpdateFieldsPatch }) =>
plans_update_fields(vars.planId, vars.patch), plans_update_fields(vars.planId, vars.patch),
onSuccess: (updated) => { onSuccess: (updated) => {
qc.setQueryData(qk.plan(updated.id), updated); qc.setQueryData(qk.plan(updated.id), updated)
qc.invalidateQueries({ queryKey: ["planes", "list"] }); qc.invalidateQueries({ queryKey: ['planes', 'list'] })
qc.invalidateQueries({ queryKey: qk.planHistorial(updated.id) }); qc.invalidateQueries({ queryKey: qk.planHistorial(updated.id) })
}, },
}); })
} }
export function useUpdatePlanMapa() { export function useUpdatePlanMapa() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (vars: { planId: UUID; ops: Array<PlanMapOperation> }) => mutationFn: (vars: { planId: UUID; ops: Array<PlanMapOperation> }) =>
@@ -194,61 +194,61 @@ export function useUpdatePlanMapa() {
// ✅ Optimista (rápida) para el caso MOVE_ASIGNATURA // ✅ Optimista (rápida) para el caso MOVE_ASIGNATURA
onMutate: async (vars) => { onMutate: async (vars) => {
await qc.cancelQueries({ queryKey: qk.planAsignaturas(vars.planId) }); await qc.cancelQueries({ queryKey: qk.planAsignaturas(vars.planId) })
const prev = qc.getQueryData<any>(qk.planAsignaturas(vars.planId)); const prev = qc.getQueryData<any>(qk.planAsignaturas(vars.planId))
// solo optimizamos MOVEs simples // solo optimizamos MOVEs simples
const moves = vars.ops.filter((x) => x.op === "MOVE_ASIGNATURA"); const moves = vars.ops.filter((x) => x.op === 'MOVE_ASIGNATURA')
if (prev && Array.isArray(prev) && moves.length) { if (prev && Array.isArray(prev) && moves.length) {
const next = prev.map((a: any) => { const next = prev.map((a: any) => {
const m = moves.find((x) => x.asignaturaId === a.id); const m = moves.find((x) => x.asignaturaId === a.id)
if (!m) return a; if (!m) return a
return { return {
...a, ...a,
numero_ciclo: m.numero_ciclo, numero_ciclo: m.numero_ciclo,
linea_plan_id: m.linea_plan_id, linea_plan_id: m.linea_plan_id,
orden_celda: m.orden_celda ?? a.orden_celda, orden_celda: m.orden_celda ?? a.orden_celda,
}; }
}); })
qc.setQueryData(qk.planAsignaturas(vars.planId), next); qc.setQueryData(qk.planAsignaturas(vars.planId), next)
} }
return { prev }; return { prev }
}, },
onError: (_err, vars, ctx) => { onError: (_err, vars, ctx) => {
if (ctx?.prev) qc.setQueryData(qk.planAsignaturas(vars.planId), ctx.prev); if (ctx?.prev) qc.setQueryData(qk.planAsignaturas(vars.planId), ctx.prev)
}, },
onSuccess: (_ok, vars) => { onSuccess: (_ok, vars) => {
qc.invalidateQueries({ queryKey: qk.planAsignaturas(vars.planId) }); qc.invalidateQueries({ queryKey: qk.planAsignaturas(vars.planId) })
qc.invalidateQueries({ queryKey: qk.planHistorial(vars.planId) }); qc.invalidateQueries({ queryKey: qk.planHistorial(vars.planId) })
}, },
}); })
} }
export function useTransitionPlanEstado() { export function useTransitionPlanEstado() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: plans_transition_state, mutationFn: plans_transition_state,
onSuccess: (_ok, vars) => { onSuccess: (_ok, vars) => {
qc.invalidateQueries({ queryKey: qk.plan(vars.planId) }); qc.invalidateQueries({ queryKey: qk.plan(vars.planId) })
qc.invalidateQueries({ queryKey: qk.planHistorial(vars.planId) }); qc.invalidateQueries({ queryKey: qk.planHistorial(vars.planId) })
qc.invalidateQueries({ queryKey: ["planes", "list"] }); qc.invalidateQueries({ queryKey: ['planes', 'list'] })
}, },
}); })
} }
export function useGeneratePlanDocumento() { export function useGeneratePlanDocumento() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (planId: UUID) => plans_generate_document(planId), mutationFn: (planId: UUID) => plans_generate_document(planId),
onSuccess: (_doc, planId) => { onSuccess: (_doc, planId) => {
qc.invalidateQueries({ queryKey: qk.planDocumento(planId) }); qc.invalidateQueries({ queryKey: qk.planDocumento(planId) })
qc.invalidateQueries({ queryKey: qk.planHistorial(planId) }); qc.invalidateQueries({ queryKey: qk.planHistorial(planId) })
}, },
}); })
} }

View File

@@ -1,11 +1,11 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { qk } from "../query/keys"; import { qk } from '../query/keys'
import type { UUID } from "../types/domain"; import type { UUID } from '../types/domain'
import type { import type {
BibliografiaUpsertInput, BibliografiaUpsertInput,
SubjectsCreateManualInput, SubjectsCreateManualInput,
SubjectsUpdateFieldsPatch, SubjectsUpdateFieldsPatch,
} from "../api/subjects.api"; } from '../api/subjects.api'
import { import {
ai_generate_subject, ai_generate_subject,
subjects_bibliografia_list, subjects_bibliografia_list,
@@ -20,147 +20,177 @@ import {
subjects_update_bibliografia, subjects_update_bibliografia,
subjects_update_contenido, subjects_update_contenido,
subjects_update_fields, subjects_update_fields,
} from "../api/subjects.api"; } from '../api/subjects.api'
export function useSubject(subjectId: UUID | null | undefined) { export function useSubject(subjectId: UUID | null | undefined) {
return useQuery({ return useQuery({
queryKey: subjectId ? qk.asignatura(subjectId) : ["asignaturas", "detail", null], queryKey: subjectId
? qk.asignatura(subjectId)
: ['asignaturas', 'detail', null],
queryFn: () => subjects_get(subjectId as UUID), queryFn: () => subjects_get(subjectId as UUID),
enabled: Boolean(subjectId), enabled: Boolean(subjectId),
}); })
} }
export function useSubjectBibliografia(subjectId: UUID | null | undefined) { export function useSubjectBibliografia(subjectId: UUID | null | undefined) {
return useQuery({ return useQuery({
queryKey: subjectId ? qk.asignaturaBibliografia(subjectId) : ["asignaturas", "bibliografia", null], queryKey: subjectId
? qk.asignaturaBibliografia(subjectId)
: ['asignaturas', 'bibliografia', null],
queryFn: () => subjects_bibliografia_list(subjectId as UUID), queryFn: () => subjects_bibliografia_list(subjectId as UUID),
enabled: Boolean(subjectId), enabled: Boolean(subjectId),
}); })
} }
export function useSubjectHistorial(subjectId: UUID | null | undefined) { export function useSubjectHistorial(subjectId: UUID | null | undefined) {
return useQuery({ return useQuery({
queryKey: subjectId ? qk.asignaturaHistorial(subjectId) : ["asignaturas", "historial", null], queryKey: subjectId
? qk.asignaturaHistorial(subjectId)
: ['asignaturas', 'historial', null],
queryFn: () => subjects_history(subjectId as UUID), queryFn: () => subjects_history(subjectId as UUID),
enabled: Boolean(subjectId), enabled: Boolean(subjectId),
}); })
} }
export function useSubjectDocumento(subjectId: UUID | null | undefined) { export function useSubjectDocumento(subjectId: UUID | null | undefined) {
return useQuery({ return useQuery({
queryKey: subjectId ? qk.asignaturaDocumento(subjectId) : ["asignaturas", "documento", null], queryKey: subjectId
? qk.asignaturaDocumento(subjectId)
: ['asignaturas', 'documento', null],
queryFn: () => subjects_get_document(subjectId as UUID), queryFn: () => subjects_get_document(subjectId as UUID),
enabled: Boolean(subjectId), enabled: Boolean(subjectId),
staleTime: 30_000, staleTime: 30_000,
}); })
} }
/* ------------------ Mutations ------------------ */ /* ------------------ Mutations ------------------ */
export function useCreateSubjectManual() { export function useCreateSubjectManual() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (payload: SubjectsCreateManualInput) => subjects_create_manual(payload), mutationFn: (payload: SubjectsCreateManualInput) =>
subjects_create_manual(payload),
onSuccess: (subject) => { onSuccess: (subject) => {
qc.setQueryData(qk.asignatura(subject.id), subject); qc.setQueryData(qk.asignatura(subject.id), subject)
qc.invalidateQueries({ queryKey: qk.planAsignaturas(subject.plan_estudio_id) }); qc.invalidateQueries({
qc.invalidateQueries({ queryKey: qk.planHistorial(subject.plan_estudio_id) }); queryKey: qk.planAsignaturas(subject.plan_estudio_id),
})
qc.invalidateQueries({
queryKey: qk.planHistorial(subject.plan_estudio_id),
})
}, },
}); })
} }
export function useGenerateSubjectAI() { export function useGenerateSubjectAI() {
return useMutation({ mutationFn: ai_generate_subject }); return useMutation({ mutationFn: ai_generate_subject })
} }
export function usePersistSubjectFromAI() { export function usePersistSubjectFromAI() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (payload: { planId: UUID; jsonMateria: any }) => subjects_persist_from_ai(payload), mutationFn: (payload: { planId: UUID; jsonMateria: any }) =>
subjects_persist_from_ai(payload),
onSuccess: (subject) => { onSuccess: (subject) => {
qc.setQueryData(qk.asignatura(subject.id), subject); qc.setQueryData(qk.asignatura(subject.id), subject)
qc.invalidateQueries({ queryKey: qk.planAsignaturas(subject.plan_estudio_id) }); qc.invalidateQueries({
qc.invalidateQueries({ queryKey: qk.planHistorial(subject.plan_estudio_id) }); queryKey: qk.planAsignaturas(subject.plan_estudio_id),
})
qc.invalidateQueries({
queryKey: qk.planHistorial(subject.plan_estudio_id),
})
}, },
}); })
} }
export function useCloneSubject() { export function useCloneSubject() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: subjects_clone_from_existing, mutationFn: subjects_clone_from_existing,
onSuccess: (subject) => { onSuccess: (subject) => {
qc.setQueryData(qk.asignatura(subject.id), subject); qc.setQueryData(qk.asignatura(subject.id), subject)
qc.invalidateQueries({ queryKey: qk.planAsignaturas(subject.plan_estudio_id) }); qc.invalidateQueries({
qc.invalidateQueries({ queryKey: qk.planHistorial(subject.plan_estudio_id) }); queryKey: qk.planAsignaturas(subject.plan_estudio_id),
})
qc.invalidateQueries({
queryKey: qk.planHistorial(subject.plan_estudio_id),
})
}, },
}); })
} }
export function useImportSubjectFromFile() { export function useImportSubjectFromFile() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: subjects_import_from_file, mutationFn: subjects_import_from_file,
onSuccess: (subject) => { onSuccess: (subject) => {
qc.setQueryData(qk.asignatura(subject.id), subject); qc.setQueryData(qk.asignatura(subject.id), subject)
qc.invalidateQueries({ queryKey: qk.planAsignaturas(subject.plan_estudio_id) }); qc.invalidateQueries({
qc.invalidateQueries({ queryKey: qk.planHistorial(subject.plan_estudio_id) }); queryKey: qk.planAsignaturas(subject.plan_estudio_id),
})
qc.invalidateQueries({
queryKey: qk.planHistorial(subject.plan_estudio_id),
})
}, },
}); })
} }
export function useUpdateSubjectFields() { export function useUpdateSubjectFields() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (vars: { subjectId: UUID; patch: SubjectsUpdateFieldsPatch }) => mutationFn: (vars: { subjectId: UUID; patch: SubjectsUpdateFieldsPatch }) =>
subjects_update_fields(vars.subjectId, vars.patch), subjects_update_fields(vars.subjectId, vars.patch),
onSuccess: (updated) => { onSuccess: (updated) => {
qc.setQueryData(qk.asignatura(updated.id), updated); qc.setQueryData(qk.asignatura(updated.id), updated)
qc.invalidateQueries({ queryKey: qk.planAsignaturas(updated.plan_estudio_id) }); qc.invalidateQueries({
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) }); queryKey: qk.planAsignaturas(updated.plan_estudio_id),
})
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) })
}, },
}); })
} }
export function useUpdateSubjectContenido() { export function useUpdateSubjectContenido() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (vars: { subjectId: UUID; unidades: any[] }) => mutationFn: (vars: { subjectId: UUID; unidades: any[] }) =>
subjects_update_contenido(vars.subjectId, vars.unidades), subjects_update_contenido(vars.subjectId, vars.unidades),
onSuccess: (updated) => { onSuccess: (updated) => {
qc.setQueryData(qk.asignatura(updated.id), updated); qc.setQueryData(qk.asignatura(updated.id), updated)
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) }); qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(updated.id) })
}, },
}); })
} }
export function useUpdateSubjectBibliografia() { export function useUpdateSubjectBibliografia() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (vars: { subjectId: UUID; entries: BibliografiaUpsertInput }) => mutationFn: (vars: { subjectId: UUID; entries: BibliografiaUpsertInput }) =>
subjects_update_bibliografia(vars.subjectId, vars.entries), subjects_update_bibliografia(vars.subjectId, vars.entries),
onSuccess: (_ok, vars) => { onSuccess: (_ok, vars) => {
qc.invalidateQueries({ queryKey: qk.asignaturaBibliografia(vars.subjectId) }); qc.invalidateQueries({
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(vars.subjectId) }); queryKey: qk.asignaturaBibliografia(vars.subjectId),
})
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(vars.subjectId) })
}, },
}); })
} }
export function useGenerateSubjectDocumento() { export function useGenerateSubjectDocumento() {
const qc = useQueryClient(); const qc = useQueryClient()
return useMutation({ return useMutation({
mutationFn: (subjectId: UUID) => subjects_generate_document(subjectId), mutationFn: (subjectId: UUID) => subjects_generate_document(subjectId),
onSuccess: (_doc, subjectId) => { onSuccess: (_doc, subjectId) => {
qc.invalidateQueries({ queryKey: qk.asignaturaDocumento(subjectId) }); qc.invalidateQueries({ queryKey: qk.asignaturaDocumento(subjectId) })
qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(subjectId) }); qc.invalidateQueries({ queryKey: qk.asignaturaHistorial(subjectId) })
}, },
}); })
} }

View File

@@ -1,40 +1,53 @@
import { usePlan } from '@/data'; import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { createFileRoute } from '@tanstack/react-router' import { Pencil, Check, X, Sparkles, AlertCircle } from 'lucide-react'
import { useState } from 'react' import { useState, useEffect } from 'react'
import type { DatosGeneralesField } from '@/types/plan' import type { DatosGeneralesField } from '@/types/plan'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { import { usePlan } from '@/data'
Pencil, // import { toast } from 'sonner' // Asegúrate de tener sonner instalado o quita la línea
Check,
X,
Sparkles,
AlertCircle
} from 'lucide-react'
//import { toast } from 'sonner' // Asegúrate de tener sonner instalado o quita la línea
export const Route = createFileRoute('/planes/$planId/_detalle/datos')({ export const Route = createFileRoute('/planes/$planId/_detalle/datos')({
component: DatosGeneralesPage, component: DatosGeneralesPage,
}) })
function DatosGeneralesPage() { const formatLabel = (key: string) => {
const {data, isFetching} = usePlan('0e0aea4d-b8b4-4e75-8279-6224c3ac769f'); const result = key.replace(/_/g, ' ')
if(!isFetching && !data) { return result.charAt(0).toUpperCase() + result.slice(1)
return <div>No se encontró el plan de estudios.</div> }
}
console.log(data);
// 1. Definimos los DATOS iniciales (Lo que antes venía por props)
const [campos, setCampos] = useState<DatosGeneralesField[]>([
{ id: '1', label: 'Objetivo General', value: 'Formar profesionales...', requerido: true, tipo: 'texto' },
{ id: '2', label: 'Perfil de Ingreso', value: 'Interés por la tecnología...', requerido: true, tipo: 'lista' },
{ id: '3', label: 'Perfil de Egreso', value: '', requerido: true, tipo: 'texto' },
])
// 2. Estados de edición function DatosGeneralesPage() {
const { planId } = Route.useParams()
const { data } = usePlan(planId)
const navigate = useNavigate()
// Inicializamos campos como un arreglo vacío
const [campos, setCampos] = useState<Array<DatosGeneralesField>>([])
const [editingId, setEditingId] = useState<string | null>(null) const [editingId, setEditingId] = useState<string | null>(null)
const [editValue, setEditValue] = useState('') const [editValue, setEditValue] = useState('')
// Efecto para transformar data?.datos en el arreglo de campos
useEffect(() => {
// 2. Validación de seguridad para sourceData
const sourceData = data?.datos
if (sourceData && typeof sourceData === 'object') {
const datosTransformados: Array<DatosGeneralesField> = Object.entries(
sourceData,
).map(([key, value], index) => ({
id: (index + 1).toString(),
label: formatLabel(key),
// Forzamos el valor a string de forma segura
value: typeof value === 'string' ? value : value?.toString() || '',
requerido: true,
tipo: 'texto',
}))
setCampos(datosTransformados)
}
console.log(data)
}, [data])
// 3. Manejadores de acciones (Ahora como funciones locales) // 3. Manejadores de acciones (Ahora como funciones locales)
const handleEdit = (campo: DatosGeneralesField) => { const handleEdit = (campo: DatosGeneralesField) => {
setEditingId(campo.id) setEditingId(campo.id)
@@ -48,54 +61,76 @@ function DatosGeneralesPage() {
const handleSave = (id: string) => { const handleSave = (id: string) => {
// Actualizamos el estado local de la lista // Actualizamos el estado local de la lista
setCampos(prev => prev.map(c => setCampos((prev) =>
c.id === id ? { ...c, value: editValue } : c prev.map((c) => (c.id === id ? { ...c, value: editValue } : c)),
)) )
setEditingId(null) setEditingId(null)
setEditValue('') setEditValue('')
//toast.success('Cambios guardados localmente') // toast.success('Cambios guardados localmente')
} }
const handleIARequest = (id: string) => { const handleIARequest = (descripcion: string) => {
//toast.info('La IA está analizando el campo ' + id) navigate({
// Aquí conectarías con tu endpoint de IA to: '/planes/$planId/iaplan',
params: {
planId: '1', // o dinámico
},
state: {
prefill: descripcion,
} as any,
})
} }
return ( return (
<div className="container mx-auto px-6 py-6 animate-in fade-in duration-500"> <div className="animate-in fade-in container mx-auto px-6 py-6 duration-500">
<div className="mb-6"> <div className="mb-6">
<h2 className="text-lg font-semibold text-foreground"> <h2 className="text-foreground text-lg font-semibold">
Datos Generales del Plan Datos Generales del Plan
</h2> </h2>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-muted-foreground mt-1 text-sm">
Información estructural y descriptiva del plan de estudios Información estructural y descriptiva del plan de estudios
</p> </p>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
{campos.map((campo) => { {campos.map((campo) => {
const isEditing = editingId === campo.id const isEditing = editingId === campo.id
return ( return (
<div <div
key={campo.id} key={campo.id}
className={`border rounded-xl transition-all ${ className={`rounded-xl border transition-all ${
isEditing ? 'border-teal-500 ring-2 ring-teal-50 shadow-lg' : 'bg-white hover:shadow-md' isEditing
? 'border-teal-500 shadow-lg ring-2 ring-teal-50'
: 'bg-white hover:shadow-md'
}`} }`}
> >
{/* Header de la Card */} {/* Header de la Card */}
<div className="flex items-center justify-between px-5 py-3 border-b bg-slate-50/50"> <div className="flex items-center justify-between border-b bg-slate-50/50 px-5 py-3">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className="font-medium text-sm text-slate-700">{campo.label}</h3> <h3 className="text-sm font-medium text-slate-700">
{campo.requerido && <span className="text-red-500 text-xs">*</span>} {campo.label}
</h3>
{campo.requerido && (
<span className="text-xs text-red-500">*</span>
)}
</div> </div>
{!isEditing && ( {!isEditing && (
<div className="flex gap-1"> <div className="flex gap-1">
<Button variant="ghost" size="icon" className="h-8 w-8 text-teal-600" onClick={() => handleIARequest(campo.id)}> <Button
variant="ghost"
size="icon"
className="h-8 w-8 text-teal-600"
onClick={() => handleIARequest(campo.value)}
>
<Sparkles size={14} /> <Sparkles size={14} />
</Button> </Button>
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => handleEdit(campo)}> <Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => handleEdit(campo)}
>
<Pencil size={14} /> <Pencil size={14} />
</Button> </Button>
</div> </div>
@@ -112,10 +147,18 @@ function DatosGeneralesPage() {
className="min-h-[120px]" className="min-h-[120px]"
/> />
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button variant="outline" size="sm" onClick={handleCancel}> <Button
variant="outline"
size="sm"
onClick={handleCancel}
>
<X size={14} className="mr-1" /> Cancelar <X size={14} className="mr-1" /> Cancelar
</Button> </Button>
<Button size="sm" className="bg-teal-600 hover:bg-teal-700" onClick={() => handleSave(campo.id)}> <Button
size="sm"
className="bg-teal-600 hover:bg-teal-700"
onClick={() => handleSave(campo.id)}
>
<Check size={14} className="mr-1" /> Guardar <Check size={14} className="mr-1" /> Guardar
</Button> </Button>
</div> </div>
@@ -123,12 +166,12 @@ function DatosGeneralesPage() {
) : ( ) : (
<div className="min-h-[100px]"> <div className="min-h-[100px]">
{campo.value ? ( {campo.value ? (
<div className="text-sm text-slate-600 leading-relaxed"> <div className="text-sm leading-relaxed text-slate-600">
{campo.tipo === 'lista' ? ( {campo.tipo === 'lista' ? (
<ul className="space-y-1"> <ul className="space-y-1">
{campo.value.split('\n').map((item, i) => ( {campo.value.split('\n').map((item, i) => (
<li key={i} className="flex gap-2"> <li key={i} className="flex gap-2">
<span className="mt-1.5 h-1.5 w-1.5 rounded-full bg-teal-500 shrink-0" /> <span className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-teal-500" />
{item} {item}
</li> </li>
))} ))}
@@ -138,7 +181,7 @@ function DatosGeneralesPage() {
)} )}
</div> </div>
) : ( ) : (
<div className="flex items-center gap-2 text-slate-400 text-sm"> <div className="flex items-center gap-2 text-sm text-slate-400">
<AlertCircle size={14} /> <AlertCircle size={14} />
<span>Sin contenido.</span> <span>Sin contenido.</span>
</div> </div>
@@ -152,4 +195,4 @@ function DatosGeneralesPage() {
</div> </div>
</div> </div>
) )
} }

View File

@@ -1,49 +1,64 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { CheckCircle2, Circle, Clock } from "lucide-react" import { CheckCircle2, Circle, Clock } from 'lucide-react'
import { Badge } from "@/components/ui/badge" import { Badge } from '@/components/ui/badge'
import { Button } from "@/components/ui/button" import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Textarea } from "@/components/ui/textarea" import { Textarea } from '@/components/ui/textarea'
import { usePlanHistorial } from '@/data/hooks/usePlans'
export const Route = createFileRoute('/planes/$planId/_detalle/flujo')({ export const Route = createFileRoute('/planes/$planId/_detalle/flujo')({
component: RouteComponent, component: RouteComponent,
}) })
function RouteComponent() { function RouteComponent() {
const { data: rawData, isLoading } = usePlanHistorial(
'0e0aea4d-b8b4-4e75-8279-6224c3ac769f',
)
console.log(rawData)
return ( return (
<div className="flex flex-col gap-6 p-6"> <div className="flex flex-col gap-6 p-6">
{/* Header Informativo (Opcional, si no viene del layout padre) */} {/* Header Informativo (Opcional, si no viene del layout padre) */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold italic">Flujo de Aprobación</h1> <h1 className="text-2xl font-bold italic">Flujo de Aprobación</h1>
<p className="text-sm text-muted-foreground">Gestiona el proceso de revisión y aprobación del plan</p> <p className="text-muted-foreground text-sm">
Gestiona el proceso de revisión y aprobación del plan
</p>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8"> <div className="grid grid-cols-1 gap-8 lg:grid-cols-3">
{/* LADO IZQUIERDO: Timeline del Flujo */} {/* LADO IZQUIERDO: Timeline del Flujo */}
<div className="lg:col-span-2 space-y-4"> <div className="space-y-4 lg:col-span-2">
{/* Estado: Completado */} {/* Estado: Completado */}
<div className="relative flex gap-4 pb-4"> <div className="relative flex gap-4 pb-4">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="rounded-full bg-green-100 p-1 text-green-600"> <div className="rounded-full bg-green-100 p-1 text-green-600">
<CheckCircle2 className="h-6 w-6" /> <CheckCircle2 className="h-6 w-6" />
</div> </div>
<div className="w-px flex-1 bg-green-200 mt-2" /> <div className="mt-2 w-px flex-1 bg-green-200" />
</div> </div>
<Card className="flex-1"> <Card className="flex-1">
<CardHeader className="flex flex-row items-center justify-between py-3"> <CardHeader className="flex flex-row items-center justify-between py-3">
<div> <div>
<CardTitle className="text-lg">Borrador</CardTitle> <CardTitle className="text-lg">Borrador</CardTitle>
<p className="text-xs text-muted-foreground">14 de enero de 2024</p> <p className="text-muted-foreground text-xs">
14 de enero de 2024
</p>
</div> </div>
<Badge variant="secondary" className="bg-green-100 text-green-700">Completado</Badge> <Badge
variant="secondary"
className="bg-green-100 text-green-700"
>
Completado
</Badge>
</CardHeader> </CardHeader>
<CardContent className="text-sm border-t pt-3"> <CardContent className="border-t pt-3 text-sm">
<p className="font-semibold text-muted-foreground mb-2">Comentarios</p> <p className="text-muted-foreground mb-2 font-semibold">
<ul className="list-disc list-inside space-y-1 text-muted-foreground"> Comentarios
</p>
<ul className="text-muted-foreground list-inside list-disc space-y-1">
<li>Documento inicial creado</li> <li>Documento inicial creado</li>
<li>Estructura base definida</li> <li>Estructura base definida</li>
</ul> </ul>
@@ -57,19 +72,27 @@ function RouteComponent() {
<div className="rounded-full bg-blue-100 p-1 text-blue-600 ring-2 ring-blue-500 ring-offset-2"> <div className="rounded-full bg-blue-100 p-1 text-blue-600 ring-2 ring-blue-500 ring-offset-2">
<Clock className="h-6 w-6" /> <Clock className="h-6 w-6" />
</div> </div>
<div className="w-px flex-1 bg-slate-200 mt-2" /> <div className="mt-2 w-px flex-1 bg-slate-200" />
</div> </div>
<Card className="flex-1 border-blue-500 bg-blue-50/10"> <Card className="flex-1 border-blue-500 bg-blue-50/10">
<CardHeader className="flex flex-row items-center justify-between py-3"> <CardHeader className="flex flex-row items-center justify-between py-3">
<div> <div>
<CardTitle className="text-lg text-blue-700">En Revisión</CardTitle> <CardTitle className="text-lg text-blue-700">
<p className="text-xs text-muted-foreground">19 de febrero de 2024</p> En Revisión
</CardTitle>
<p className="text-muted-foreground text-xs">
19 de febrero de 2024
</p>
</div> </div>
<Badge variant="default" className="bg-blue-500">En curso</Badge> <Badge variant="default" className="bg-blue-500">
En curso
</Badge>
</CardHeader> </CardHeader>
<CardContent className="text-sm border-t border-blue-100 pt-3"> <CardContent className="border-t border-blue-100 pt-3 text-sm">
<p className="font-semibold text-muted-foreground mb-2">Comentarios</p> <p className="text-muted-foreground mb-2 font-semibold">
<ul className="list-disc list-inside space-y-1 text-muted-foreground"> Comentarios
</p>
<ul className="text-muted-foreground list-inside list-disc space-y-1">
<li>Revisión de objetivo general pendiente</li> <li>Revisión de objetivo general pendiente</li>
<li>Mapa curricular aprobado preliminarmente</li> <li>Mapa curricular aprobado preliminarmente</li>
</ul> </ul>
@@ -91,7 +114,6 @@ function RouteComponent() {
</CardHeader> </CardHeader>
</Card> </Card>
</div> </div>
</div> </div>
{/* LADO DERECHO: Formulario de Transición */} {/* LADO DERECHO: Formulario de Transición */}
@@ -101,22 +123,24 @@ function RouteComponent() {
<CardTitle className="text-lg">Transición de Estado</CardTitle> <CardTitle className="text-lg">Transición de Estado</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-6"> <CardContent className="space-y-6">
<div className="flex items-center justify-between p-3 bg-slate-50 rounded-lg text-sm border"> <div className="flex items-center justify-between rounded-lg border bg-slate-50 p-3 text-sm">
<div className="text-center"> <div className="text-center">
<p className="text-xs text-muted-foreground">Estado actual</p> <p className="text-muted-foreground text-xs">Estado actual</p>
<p className="font-bold">En Revisión</p> <p className="font-bold">En Revisión</p>
</div> </div>
<div className="h-px flex-1 bg-slate-300 mx-4" /> <div className="mx-4 h-px flex-1 bg-slate-300" />
<div className="text-center"> <div className="text-center">
<p className="text-xs text-muted-foreground">Siguiente</p> <p className="text-muted-foreground text-xs">Siguiente</p>
<p className="font-bold text-primary">Revisión Expertos</p> <p className="text-primary font-bold">Revisión Expertos</p>
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<label className="text-sm font-medium">Comentario de transición</label> <label className="text-sm font-medium">
<Textarea Comentario de transición
placeholder="Agrega un comentario para la transición..." </label>
<Textarea
placeholder="Agrega un comentario para la transición..."
className="min-h-[120px]" className="min-h-[120px]"
/> />
</div> </div>
@@ -127,8 +151,7 @@ function RouteComponent() {
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
</div> </div>
</div> </div>
) )
} }

View File

@@ -1,142 +1,284 @@
import { useMemo, useState } from 'react'
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { import {
GitBranch, GitBranch,
Edit3, Edit3,
PlusCircle, PlusCircle,
FileText, RefreshCw,
RefreshCw, User,
User Loader2,
} from "lucide-react" Clock,
import { Badge } from "@/components/ui/badge" Eye,
import { Card, CardContent } from "@/components/ui/card" History,
import { Avatar, AvatarFallback } from "@/components/ui/avatar" Calendar,
} from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Card, CardContent } from '@/components/ui/card'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { usePlanHistorial } from '@/data/hooks/usePlans'
import { format, formatDistanceToNow, parseISO } from 'date-fns'
import { es } from 'date-fns/locale'
export const Route = createFileRoute('/planes/$planId/_detalle/historial')({ export const Route = createFileRoute('/planes/$planId/_detalle/historial')({
component: RouteComponent, component: RouteComponent,
}) })
function RouteComponent() { const getEventConfig = (tipo: string, campo: string) => {
const historyEvents = [ if (tipo === 'CREACION')
{ return {
id: 1, label: 'Creación',
type: 'Cambio de estado',
user: 'Dr. Juan Pérez',
description: 'Plan pasado de Borrador a En Revisión',
date: 'Hace 2 días',
icon: <GitBranch className="h-4 w-4" />,
details: { from: 'Borrador', to: 'En Revisión' }
},
{
id: 2,
type: 'Edición',
user: 'Lic. María García',
description: 'Actualizado perfil de egreso',
date: 'Hace 3 días',
icon: <Edit3 className="h-4 w-4" />,
},
{
id: 3,
type: 'Reorganización',
user: 'Ing. Carlos López',
description: 'Movida materia BD102 de ciclo 3 a ciclo 4',
date: 'Hace 5 días',
icon: <RefreshCw className="h-4 w-4" />,
details: { from: 'Ciclo 3', to: 'Ciclo 4' }
},
{
id: 4,
type: 'Creación',
user: 'Dr. Juan Pérez',
description: 'Añadida nueva materia: Inteligencia Artificial',
date: 'Hace 1 semana',
icon: <PlusCircle className="h-4 w-4" />, icon: <PlusCircle className="h-4 w-4" />,
}, color: 'teal',
{
id: 5,
type: 'Documento',
user: 'Lic. María García',
description: 'Generado documento oficial v1.0',
date: 'Hace 1 semana',
icon: <FileText className="h-4 w-4" />,
} }
] if (campo === 'estado')
return {
label: 'Cambio de estado',
icon: <GitBranch className="h-4 w-4" />,
color: 'blue',
}
if (campo === 'datos')
return {
label: 'Edición de Datos',
icon: <Edit3 className="h-4 w-4" />,
color: 'amber',
}
return {
label: 'Actualización',
icon: <RefreshCw className="h-4 w-4" />,
color: 'slate',
}
}
function RouteComponent() {
const { planId } = Route.useParams()
const { data: rawData, isLoading } = usePlanHistorial(
'0e0aea4d-b8b4-4e75-8279-6224c3ac769f',
)
// ESTADOS PARA EL MODAL
const [selectedEvent, setSelectedEvent] = useState<any>(null)
const [isModalOpen, setIsModalOpen] = useState(false)
const historyEvents = useMemo(() => {
if (!rawData) return []
return rawData.map((item: any) => {
const config = getEventConfig(item.tipo, item.campo)
return {
id: item.id,
type: config.label,
user:
item.cambiado_por === '11111111-1111-1111-1111-111111111111'
? 'Administrador'
: 'Usuario Staff',
description:
item.campo === 'datos'
? `Actualización general de: ${item.valor_nuevo?.nombre || 'información del plan'}`
: `Se modificó el campo ${item.campo}`,
date: parseISO(item.cambiado_en),
icon: config.icon,
campo: item.campo,
details: {
from: item.valor_anterior,
to: item.valor_nuevo,
},
}
})
}, [rawData])
const openCompareModal = (event: any) => {
setSelectedEvent(event)
setIsModalOpen(true)
}
const renderValue = (val: any) => {
if (!val) return 'Sin información'
if (typeof val === 'object') return JSON.stringify(val, null, 2)
return String(val)
}
if (isLoading)
return (
<div className="flex h-64 items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin text-teal-600" />
</div>
)
return ( return (
<div className="p-6 max-w-5xl mx-auto"> <div className="mx-auto max-w-5xl p-6">
<div className="mb-8"> <div className="mb-8 flex items-end justify-between">
<h1 className="text-xl font-bold text-slate-800">Historial de Cambios</h1> <div>
<p className="text-sm text-muted-foreground">Registro de todas las modificaciones realizadas al plan</p> <h1 className="flex items-center gap-2 text-xl font-bold text-slate-800">
<Clock className="h-5 w-5 text-teal-600" /> Historial de Cambios del
Plan
</h1>
<p className="text-muted-foreground text-sm">
Registro cronológico de modificaciones realizadas
</p>
</div>
</div> </div>
<div className="relative space-y-0"> <div className="relative space-y-0">
{/* Línea vertical de fondo */} <div className="absolute top-0 bottom-0 left-9 w-px bg-slate-200" />
<div className="absolute left-9 top-0 bottom-0 w-px bg-slate-200" /> {historyEvents.length === 0 ? (
<div className="ml-20 py-10 text-slate-500">No hay registros.</div>
) : (
historyEvents.map((event) => (
<div key={event.id} className="group relative flex gap-6 pb-8">
<div className="relative z-10 flex h-18 flex-col items-center">
<div className="flex h-[42px] w-[42px] items-center justify-center rounded-full border-4 border-white bg-slate-100 text-slate-600 shadow-sm transition-colors group-hover:bg-teal-50 group-hover:text-teal-600">
{event.icon}
</div>
</div>
{historyEvents.map((event) => ( <Card className="flex-1 border-slate-200 shadow-none transition-colors hover:border-teal-200">
<div key={event.id} className="relative flex gap-6 pb-8 group"> <CardContent className="p-4">
<div className="flex flex-col gap-1">
{/* Indicador con Icono */} {/* LÍNEA SUPERIOR: Título a la izquierda --- Usuario, Botón y Fecha a la derecha */}
<div className="relative z-10 flex h-18 flex-col items-center"> <div className="flex items-center justify-between">
<div className="flex h-[42px] w-[42px] items-center justify-center rounded-full border-4 border-white bg-slate-100 text-slate-600 shadow-sm group-hover:bg-teal-50 group-hover:text-teal-600 transition-colors"> <div className="flex items-center gap-2">
{event.icon} <span className="text-sm font-bold text-slate-800">
{event.type}
</span>
<Badge
variant="outline"
className="h-5 py-0 text-[10px] font-normal"
>
{formatDistanceToNow(event.date, {
addSuffix: true,
locale: es,
})}
</Badge>
</div>
{/* Grupo de elementos alineados a la derecha */}
<div className="flex items-center gap-4 text-slate-500">
{/* Usuario e Icono */}
<div className="flex items-center gap-2 text-xs">
<User className="h-3.5 w-3.5" />
<span className="text-muted-foreground">
{event.user}
</span>
</div>
{/* Botón Ver Cambios */}
<button
onClick={() => openCompareModal(event)}
className="group/btn flex items-center gap-1.5 text-xs transition-colors hover:text-teal-600"
>
<Eye className="h-4 w-4 text-slate-400 group-hover/btn:text-teal-600" />
<span>Ver cambios</span>
</button>
{/* Fecha exacta (Solo visible en desktop para no amontonar) */}
<span className="hidden text-[11px] text-slate-400 md:block">
{format(event.date, 'yyyy-MM-dd HH:mm')}
</span>
</div>
</div>
{/* LÍNEA INFERIOR: Descripción */}
<div className="mt-1">
<p className="text-sm text-slate-600">
{event.description}
</p>
{/* Badges de transición opcionales (de estado) */}
{event.details &&
typeof event.details.from === 'string' &&
event.campo === 'estado' && (
<div className="mt-2 flex items-center gap-1.5">
<Badge
variant="secondary"
className="bg-red-50 px-1.5 text-[9px] text-red-700"
>
{event.details.from}
</Badge>
<span className="text-[10px] text-slate-400">
</span>
<Badge
variant="secondary"
className="bg-emerald-50 px-1.5 text-[9px] text-emerald-700"
>
{event.details.to}
</Badge>
</div>
)}
</div>
</div>
</CardContent>
</Card>
</div>
))
)}
</div>
{/* MODAL DE COMPARACIÓN CON SCROLL INTERNO */}
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
<DialogContent className="flex max-h-[90vh] max-w-4xl flex-col gap-0 overflow-hidden p-0">
<DialogHeader className="border-b bg-slate-50/50 p-6">
<DialogTitle className="flex items-center gap-2">
<History className="h-5 w-5 text-teal-600" /> Comparación de
Versiones
</DialogTitle>
<div className="text-muted-foreground flex items-center gap-4 pt-2 text-xs">
<span className="flex items-center gap-1">
<User className="h-3 w-3" /> {selectedEvent?.user}
</span>
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />{' '}
{selectedEvent &&
format(selectedEvent.date, "d 'de' MMMM, HH:mm", {
locale: es,
})}
</span>
</div>
</DialogHeader>
<div className="flex-1 overflow-y-auto p-6">
<div className="grid h-full grid-cols-2 gap-6">
{/* Lado Antes */}
<div className="flex flex-col space-y-2">
<div className="sticky top-0 z-10 flex items-center gap-2 bg-white py-1">
<div className="h-2 w-2 rounded-full bg-red-400" />
<span className="text-muted-foreground text-[10px] font-bold tracking-widest uppercase">
Versión Anterior
</span>
</div>
<div className="max-h-[500px] min-h-[250px] flex-1 overflow-y-auto rounded-lg border border-red-100 bg-red-50/30 p-4 font-mono text-xs leading-relaxed whitespace-pre-wrap text-slate-700">
{renderValue(selectedEvent?.details.from)}
</div>
</div>
{/* Lado Después */}
<div className="flex flex-col space-y-2">
<div className="sticky top-0 z-10 flex items-center gap-2 bg-white py-1">
<div className="h-2 w-2 rounded-full bg-emerald-400" />
<span className="text-muted-foreground text-[10px] font-bold tracking-widest uppercase">
Nueva Versión
</span>
</div>
<div className="max-h-[500px] min-h-[250px] flex-1 overflow-y-auto rounded-lg border border-emerald-100 bg-emerald-50/30 p-4 font-mono text-xs leading-relaxed whitespace-pre-wrap text-slate-700">
{renderValue(selectedEvent?.details.to)}
</div>
</div> </div>
</div> </div>
{/* Tarjeta de Contenido */}
<Card className="flex-1 shadow-none border-slate-200 hover:border-teal-200 transition-colors">
<CardContent className="p-4">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-2 mb-2">
<div className="flex items-center gap-2">
<span className="font-bold text-slate-800 text-sm">{event.type}</span>
<Badge variant="outline" className="text-[10px] font-normal py-0">
{event.date}
</Badge>
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground">
<Avatar className="h-5 w-5 border">
<AvatarFallback className="text-[8px] bg-slate-50"><User size={10}/></AvatarFallback>
</Avatar>
{event.user}
</div>
</div>
<p className="text-sm text-slate-600 mb-3">{event.description}</p>
{/* Badges de transición (si existen) */}
{event.details && (
<div className="flex items-center gap-2 mt-2">
<Badge variant="secondary" className="bg-orange-50 text-orange-700 hover:bg-orange-50 border-orange-100 text-[10px]">
{event.details.from}
</Badge>
<span className="text-slate-400 text-xs"></span>
<Badge variant="secondary" className="bg-green-50 text-green-700 hover:bg-green-50 border-green-100 text-[10px]">
{event.details.to}
</Badge>
</div>
)}
</CardContent>
</Card>
</div> </div>
))}
{/* Evento inicial de creación */} <div className="flex justify-center border-t bg-slate-50 p-4">
<div className="relative flex gap-6 group"> <Badge variant="outline" className="font-mono text-[10px]">
<div className="relative z-10 flex items-center"> Campo: {selectedEvent?.campo}
<div className="flex h-[42px] w-[42px] items-center justify-center rounded-full border-4 border-white bg-teal-600 text-white shadow-sm"> </Badge>
<PlusCircle className="h-4 w-4" />
</div>
</div> </div>
<Card className="flex-1 bg-teal-50/30 border-teal-100 shadow-none"> </DialogContent>
<CardContent className="p-4"> </Dialog>
<div className="flex items-center justify-between mb-1">
<span className="font-bold text-teal-900 text-sm">Creación</span>
<span className="text-[10px] text-teal-600 font-medium">14 Ene 2024</span>
</div>
<p className="text-sm text-teal-800/80">Plan de estudios creado</p>
</CardContent>
</Card>
</div>
</div>
</div> </div>
) )
} }

View File

@@ -1,118 +1,371 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute, useRouterState } from '@tanstack/react-router'
import { Sparkles, Send, Paperclip, Target, UserCheck, Lightbulb, FileText } from "lucide-react" import {
import { useState } from 'react' // Importamos useState Sparkles,
import { Button } from "@/components/ui/button" Send,
import { Input } from "@/components/ui/input" Target,
import { ScrollArea } from "@/components/ui/scroll-area" UserCheck,
import { Avatar, AvatarFallback } from "@/components/ui/avatar" Lightbulb,
FileText,
GraduationCap,
BookOpen,
Check,
X,
} from 'lucide-react'
import { useState, useEffect, useRef, useMemo } from 'react'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Button } from '@/components/ui/button'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Textarea } from '@/components/ui/textarea'
import { usePlan } from '@/data/hooks/usePlans'
const PRESETS = [
{
id: 'objetivo',
label: 'Mejorar objetivo general',
icon: Target,
prompt: 'Mejora la redacción del objetivo general...',
},
{
id: 'perfil-egreso',
label: 'Redactar perfil de egreso',
icon: GraduationCap,
prompt: 'Genera un perfil de egreso detallado...',
},
{
id: 'competencias',
label: 'Sugerir competencias',
icon: BookOpen,
prompt: 'Genera una lista de competencias...',
},
{
id: 'pertinencia',
label: 'Justificar pertinencia',
icon: FileText,
prompt: 'Redacta una justificación de pertinencia...',
},
]
// --- Tipado y Helpers ---
interface SelectedField {
key: string
label: string
value: string
}
const formatLabel = (key: string) => {
const result = key.replace(/_/g, ' ')
return result.charAt(0).toUpperCase() + result.slice(1)
}
export const Route = createFileRoute('/planes/$planId/_detalle/iaplan')({ export const Route = createFileRoute('/planes/$planId/_detalle/iaplan')({
component: RouteComponent, component: RouteComponent,
}) })
function RouteComponent() { function RouteComponent() {
// 1. Estado para el texto del input const { planId } = Route.useParams()
const [inputValue, setInputValue] = useState('') // Usamos el ID dinámico del plan o el hardcoded según tu necesidad
const { data } = usePlan('0e0aea4d-b8b4-4e75-8279-6224c3ac769f')
// 2. Estado para la lista de mensajes (iniciamos con los de la imagen) const routerState = useRouterState()
const [messages, setMessages] = useState([
{ id: 1, role: 'ai', text: 'Hola, soy tu asistente de IA para el diseño del plan de estudios...' }, // ESTADOS PRINCIPALES
{ id: 2, role: 'user', text: 'jkasakj' }, const [messages, setMessages] = useState<Array<any>>([
{ id: 3, role: 'ai', text: 'Entendido. Estoy procesando tu solicitud.' }, {
id: '1',
role: 'assistant',
content:
'¡Hola! Soy tu asistente de IA. ¿Qué campos deseas mejorar? Puedes escribir ":" para seleccionar uno.',
},
]) ])
const [input, setInput] = useState('')
const [selectedFields, setSelectedFields] = useState<Array<SelectedField>>([])
const [showSuggestions, setShowSuggestions] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [pendingSuggestion, setPendingSuggestion] = useState<any>(null)
// 3. Función para enviar el mensaje const scrollRef = useRef<HTMLDivElement>(null)
const handleSend = () => {
if (!inputValue.trim()) return
// Agregamos el mensaje del usuario // 1. Transformar datos de la API para el menú de selección
const newMessage = { const availableFields = useMemo(() => {
id: Date.now(), if (!data?.datos) return []
role: 'user', return Object.entries(data.datos).map(([key, value]) => ({
text: inputValue key,
label: formatLabel(key),
value: String(value || ''),
}))
}, [data])
// 2. Manejar el estado inicial si viene de "Datos Generales"
useEffect(() => {
const state = routerState.location.state as any
if (state?.prefill && availableFields.length > 0) {
// Intentamos encontrar qué campo es por su valor o si mandaste el fieldKey
const field = availableFields.find(
(f) => f.value === state.prefill || f.key === state.fieldKey,
)
if (field && !selectedFields.find((sf) => sf.key === field.key)) {
setSelectedFields([field])
}
setInput(`Mejora este campo: `)
} }
}, [availableFields])
setMessages([...messages, newMessage]) // 3. Lógica para el disparador ":"
setInputValue('') // Limpiamos el input const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const val = e.target.value
setInput(val)
if (val.endsWith(':')) {
setShowSuggestions(true)
} else {
setShowSuggestions(false)
}
}
const toggleField = (field: SelectedField) => {
setSelectedFields((prev) =>
prev.find((f) => f.key === field.key)
? prev.filter((f) => f.key !== field.key)
: [...prev, field],
)
if (input.endsWith(':')) setInput(input.slice(0, -1))
setShowSuggestions(false)
}
const handleSend = async (promptOverride?: string) => {
const textToSend = promptOverride || input
if (!textToSend.trim() && selectedFields.length === 0) return
const userMsg = {
id: Date.now().toString(),
role: 'user',
content: textToSend,
}
setMessages((prev) => [...prev, userMsg])
setInput('')
setIsLoading(true)
// Aquí simularías la llamada a la API enviando 'selectedFields' como contexto
setTimeout(() => {
const mockText =
'Sugerencia generada basada en los campos seleccionados...'
setMessages((prev) => [
...prev,
{
id: Date.now().toString(),
role: 'assistant',
content: `He analizado ${selectedFields.length > 0 ? selectedFields.map((f) => f.label).join(', ') : 'tu solicitud'}. Aquí tienes una propuesta:\n\n${mockText}`,
},
])
setPendingSuggestion({ text: mockText })
setIsLoading(false)
}, 1200)
} }
return ( return (
<div className="flex h-[calc(100vh-200px)] gap-6 p-4"> <div className="flex h-[calc(100vh-160px)] max-h-[calc(100vh-160px)] w-full gap-6 overflow-hidden p-4">
<div className="flex flex-col flex-1 bg-slate-50/50 rounded-xl border relative overflow-hidden"> {/* PANEL DE CHAT PRINCIPAL */}
<div className="relative flex min-w-0 flex-[3] flex-col overflow-hidden rounded-xl border border-slate-200 bg-slate-50/50 shadow-sm">
<ScrollArea className="flex-1 p-6"> {/* NUEVO: Barra superior de campos seleccionados */}
<div className="space-y-6 max-w-3xl mx-auto"> <div className="shrink-0 border-b bg-white p-3">
{/* 4. Mapeamos los mensajes dinámicamente */} <div className="flex flex-wrap items-center gap-2">
{messages.map((msg) => ( <span className="text-[10px] font-bold text-slate-400 uppercase">
<div key={msg.id} className={`flex ${msg.role === 'user' ? 'flex-row-reverse' : 'flex-row'} gap-3`}> Campos a mejorar:
{msg.role === 'ai' && ( </span>
<Avatar className="h-8 w-8 border bg-teal-50"> {selectedFields.map((field) => (
<AvatarFallback className="text-teal-600"><Sparkles size={16}/></AvatarFallback> <div
</Avatar> key={field.key}
)} className="animate-in zoom-in-95 flex items-center gap-1.5 rounded-lg border border-teal-100 bg-teal-50 px-2 py-1 text-xs font-medium text-teal-700"
>
<div className={msg.role === 'ai' ? 'space-y-2' : ''}> {field.label}
{msg.role === 'ai' && <p className="text-xs font-bold text-teal-700 uppercase tracking-wider">Asistente IA</p>} <button
<div className={`p-4 rounded-2xl text-sm shadow-sm ${ onClick={() => toggleField(field)}
msg.role === 'user' className="hover:text-red-500"
? 'bg-teal-600 text-white rounded-tr-none' >
: 'bg-white border text-slate-700 rounded-tl-none' <X size={12} />
}`}> </button>
{msg.text}
</div>
</div>
</div> </div>
))} ))}
{selectedFields.length === 0 && (
<span className="text-xs text-slate-400 italic">
Escribe ":" para añadir campos
</span>
)}
</div> </div>
</ScrollArea> </div>
{/* 5. Input vinculado al estado */} {/* CONTENIDO DEL CHAT */}
<div className="p-4 bg-white border-t"> <div className="relative min-h-0 flex-1">
<div className="max-w-4xl mx-auto flex gap-2 items-center bg-slate-50 border rounded-lg px-3 py-1 shadow-sm focus-within:ring-1 focus-within:ring-teal-500 transition-all"> <ScrollArea ref={scrollRef} className="h-full w-full">
<Input <div className="mx-auto max-w-3xl space-y-6 p-6">
value={inputValue} {messages.map((msg) => (
onChange={(e) => setInputValue(e.target.value)} <div
onKeyDown={(e) => e.key === 'Enter' && handleSend()} // Enviar con Enter key={msg.id}
className="border-none bg-transparent focus-visible:ring-0 text-sm" className={`flex ${msg.role === 'user' ? 'flex-row-reverse' : 'flex-row'} items-start gap-3`}
placeholder='Escribe tu solicitud... Usa ":" para mencionar campos' >
/> <Avatar
<Button variant="ghost" size="icon" className="text-slate-400"> className={`h-8 w-8 shrink-0 border ${msg.role === 'assistant' ? 'bg-teal-50' : 'bg-slate-200'}`}
<Paperclip size={18} /> >
</Button> <AvatarFallback className="text-[10px]">
<Button {msg.role === 'assistant' ? (
onClick={handleSend} <Sparkles size={14} className="text-teal-600" />
size="icon" ) : (
className="bg-teal-600 hover:bg-teal-700 h-8 w-8" <UserCheck size={14} />
> )}
<Send size={16} /> </AvatarFallback>
</Button> </Avatar>
<div
className={`flex max-w-[85%] flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}
>
<div
className={`rounded-2xl p-3 text-sm whitespace-pre-wrap shadow-sm ${
msg.role === 'user'
? 'rounded-tr-none bg-teal-600 text-white'
: 'rounded-tl-none border bg-white text-slate-700'
}`}
>
{msg.content}
</div>
</div>
</div>
))}
{isLoading && (
<div className="flex gap-2 p-4">
<div className="h-2 w-2 animate-bounce rounded-full bg-teal-400" />
<div className="h-2 w-2 animate-bounce rounded-full bg-teal-400 [animation-delay:0.2s]" />
<div className="h-2 w-2 animate-bounce rounded-full bg-teal-400 [animation-delay:0.4s]" />
</div>
)}
</div>
</ScrollArea>
{/* Botones flotantes de aplicación */}
{pendingSuggestion && !isLoading && (
<div className="animate-in fade-in slide-in-from-bottom-2 absolute bottom-4 left-1/2 z-10 flex -translate-x-1/2 gap-2 rounded-full border bg-white p-1.5 shadow-2xl">
<Button
variant="ghost"
size="sm"
onClick={() => setPendingSuggestion(null)}
className="h-8 rounded-full text-xs"
>
<X className="mr-1 h-3 w-3" /> Descartar
</Button>
<Button
size="sm"
className="h-8 rounded-full bg-teal-600 text-xs text-white hover:bg-teal-700"
>
<Check className="mr-1 h-3 w-3" /> Aplicar cambios
</Button>
</div>
)}
</div>
{/* INPUT FIJO AL FONDO CON SUGERENCIAS : */}
{/* INPUT FIJO AL FONDO CON SUGERENCIAS : */}
<div className="shrink-0 border-t bg-white p-4">
<div className="relative mx-auto max-w-4xl">
{/* MENÚ DE SUGERENCIAS FLOTANTE (Se mantiene igual) */}
{showSuggestions && (
<div className="animate-in slide-in-from-bottom-2 absolute bottom-full z-50 mb-2 w-72 overflow-hidden rounded-xl border bg-white shadow-2xl">
<div className="border-b bg-slate-50 px-3 py-2 text-[10px] font-bold tracking-wider text-slate-500 uppercase">
Seleccionar campo para IA
</div>
<div className="max-h-64 overflow-y-auto p-1">
{availableFields.map((field) => (
<button
key={field.key}
onClick={() => toggleField(field)}
className="group flex w-full items-center justify-between rounded-lg px-3 py-2 text-left text-sm transition-colors hover:bg-teal-50"
>
<span className="text-slate-700 group-hover:text-teal-700">
{field.label}
</span>
{selectedFields.find((f) => f.key === field.key) && (
<Check size={14} className="text-teal-600" />
)}
</button>
))}
</div>
</div>
)}
{/* CONTENEDOR DEL INPUT TRANSFORMADO */}
<div className="flex flex-col gap-2 rounded-xl border bg-slate-50 p-2 transition-all focus-within:bg-white focus-within:ring-1 focus-within:ring-teal-500">
{/* 1. Visualización de campos dentro del input (Tags) */}
{selectedFields.length > 0 && (
<div className="flex flex-wrap gap-2 px-2 pt-1">
{selectedFields.map((field) => (
<div
key={field.key}
className="animate-in zoom-in-95 flex items-center gap-1 rounded-md border border-teal-200 bg-teal-100 px-2 py-0.5 text-[11px] font-semibold text-teal-800"
>
<span className="opacity-70">Campo:</span> {field.label}
<button
onClick={() => toggleField(field)}
className="ml-1 rounded-full p-0.5 transition-colors hover:bg-teal-200"
>
<X size={10} />
</button>
</div>
))}
</div>
)}
{/* 2. Área de escritura */}
<div className="flex items-end gap-2">
<Textarea
value={input}
onChange={handleInputChange}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
handleSend()
}
}}
placeholder={
selectedFields.length > 0
? 'Escribe instrucciones adicionales...'
: 'Escribe tu solicitud o ":" para campos...'
}
className="max-h-[120px] min-h-[40px] flex-1 resize-none border-none bg-transparent py-2 text-sm shadow-none focus-visible:ring-0"
/>
<Button
onClick={() => handleSend()}
disabled={
(!input.trim() && selectedFields.length === 0) || isLoading
}
size="icon"
className="mb-1 h-9 w-9 shrink-0 bg-teal-600 hover:bg-teal-700"
>
<Send size={16} className="text-white" />
</Button>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
{/* Panel lateral (se mantiene igual) */} {/* PANEL LATERAL (PRESETS) - SE MANTIENE COMO LO TENÍAS */}
<div className="w-72 space-y-4"> <div className="flex flex-[1] flex-col gap-4 overflow-y-auto pr-2">
<div className="flex items-center gap-2 text-orange-500 font-semibold text-sm mb-4"> <h4 className="flex items-center gap-2 text-left text-sm font-bold text-slate-800">
<Lightbulb size={18} /> <Lightbulb size={18} className="text-orange-500" /> Acciones rápidas
Acciones rápidas </h4>
</div>
<div className="space-y-2"> <div className="space-y-2">
<ActionButton icon={<Target className="text-teal-500" size={18} />} text="Mejorar objetivo general" /> {PRESETS.map((preset) => (
<ActionButton icon={<UserCheck className="text-slate-500" size={18} />} text="Redactar perfil de egreso" /> <button
<ActionButton icon={<Lightbulb className="text-blue-500" size={18} />} text="Sugerir competencias" /> key={preset.id}
<ActionButton icon={<FileText className="text-teal-500" size={18} />} text="Justificar pertinencia" /> onClick={() => handleSend(preset.prompt)}
className="group flex w-full items-center gap-3 rounded-xl border bg-white p-3 text-left text-sm shadow-sm transition-all hover:border-teal-500 hover:bg-teal-50"
>
<div className="rounded-lg bg-slate-100 p-2 text-slate-500 group-hover:bg-teal-100 group-hover:text-teal-600">
<preset.icon size={16} />
</div>
<span className="leading-tight font-medium text-slate-700">
{preset.label}
</span>
</button>
))}
</div> </div>
</div> </div>
</div> </div>
) )
} }
function ActionButton({ icon, text }: { icon: React.ReactNode, text: string }) {
return (
<Button variant="outline" className="w-full justify-start gap-3 h-auto py-3 px-4 text-sm font-normal hover:bg-slate-50 border-slate-200 shadow-sm text-slate-700">
{icon}
{text}
</Button>
)
}

View File

@@ -1,302 +1,725 @@
/* eslint-disable jsx-a11y/label-has-associated-control */
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute } from '@tanstack/react-router'
import { useState } from 'react' import {
import { Badge } from '@/components/ui/badge' Plus,
import { Input } from '@/components/ui/input' ChevronDown,
import { AlertTriangle,
Plus,
ChevronDown,
AlertTriangle,
GripVertical, GripVertical,
Trash2 Trash2,
} from 'lucide-react' } from 'lucide-react'
import { useMemo, useState, useEffect } from 'react'
import type { Materia, LineaCurricular } from '@/types/plan' import type { Materia, LineaCurricular } from '@/types/plan'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { import {
Dialog, DialogContent, DialogHeader, DialogTitle Dialog,
} from "@/components/ui/dialog" DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu" } from '@/components/ui/dropdown-menu'
import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { usePlanAsignaturas, usePlanLineas } from '@/data'
export const Route = createFileRoute('/planes/$planId/_detalle/mapa')({ // --- Mapeadores (Fuera del componente para mayor limpieza) ---
component: MapaCurricularPage, const mapLineasToLineaCurricular = (
}) lineasApi: Array<any> = [],
): Array<LineaCurricular> => {
return lineasApi.map((linea) => ({
id: linea.id,
nombre: linea.nombre,
orden: linea.orden ?? 0,
color: '#1976d2',
}))
}
// --- Constantes de Estilo y Datos --- const mapAsignaturasToMaterias = (asigApi: Array<any> = []): Array<Materia> => {
const INITIAL_LINEAS: LineaCurricular[] = [ return asigApi.map((asig) => ({
{ id: 'l1', nombre: 'Formación Básica', orden: 1 }, id: asig.id,
{ id: 'l2', nombre: 'Ciencias de la Computación', orden: 2 }, clave: asig.codigo,
]; nombre: asig.nombre,
creditos: asig.creditos ?? 0,
const INITIAL_MATERIAS: Materia[] = [ ciclo: asig.numero_ciclo ?? null,
{ id: "1", clave: 'MAT101', nombre: 'Cálculo Diferencial', creditos: 8, hd: 4, hi: 4, ciclo: 1, lineaCurricularId: 'l1', tipo: 'obligatoria', estado: 'aprobada' }, lineaCurricularId: asig.linea_plan_id ?? null,
{ id: "2", clave: 'FIS101', nombre: 'Física Mecánica', creditos: 6, hd: 3, hi: 3, ciclo: 1, lineaCurricularId: 'l1', tipo: 'obligatoria', estado: 'aprobada' }, tipo: asig.tipo === 'OBLIGATORIA' ? 'obligatoria' : 'optativa',
{ id: "3", clave: 'PRO101', nombre: 'Fundamentos de Programación', creditos: 8, hd: 4, hi: 4, ciclo: null, lineaCurricularId: null, tipo: 'obligatoria', estado: 'borrador' }, estado: 'borrador',
]; orden: asig.orden_celda ?? 0,
hd: Math.floor((asig.horas_semana ?? 0) / 2),
hi: Math.ceil((asig.horas_semana ?? 0) / 2),
prerrequisitos: [],
}))
}
const lineColors = [ const lineColors = [
'bg-blue-50 border-blue-200 text-blue-700', 'bg-blue-50 border-blue-200 text-blue-700',
'bg-purple-50 border-purple-200 text-purple-700', 'bg-purple-50 border-purple-200 text-purple-700',
'bg-orange-50 border-orange-200 text-orange-700', 'bg-orange-50 border-orange-200 text-orange-700',
'bg-emerald-50 border-emerald-200 text-emerald-700', 'bg-emerald-50 border-emerald-200 text-emerald-700',
]; ]
const statusBadge: Record<string, string> = { const statusBadge: Record<string, string> = {
borrador: 'bg-slate-100 text-slate-600', borrador: 'bg-slate-100 text-slate-600',
revisada: 'bg-amber-100 text-amber-700', revisada: 'bg-amber-100 text-amber-700',
aprobada: 'bg-emerald-100 text-emerald-700', aprobada: 'bg-emerald-100 text-emerald-700',
}; }
// --- Subcomponentes --- // --- Subcomponentes ---
function StatItem({ label, value, total }: { label: string, value: number, total?: number }) { function StatItem({
label,
value,
total,
}: {
label: string
value: number
total?: number
}) {
return ( return (
<div className="flex items-baseline gap-1.5"> <div className="flex items-baseline gap-1.5">
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">{label}:</span> <span className="text-[10px] font-bold tracking-wider text-slate-400 uppercase">
{label}:
</span>
<span className="text-sm font-bold text-slate-700"> <span className="text-sm font-bold text-slate-700">
{value}{total ? <span className="text-slate-400 font-normal">/{total}</span> : ''} {value}
{total ? (
<span className="font-normal text-slate-400">/{total}</span>
) : (
''
)}
</span> </span>
</div> </div>
) )
} }
function MateriaCardItem({ materia, onDragStart, isDragging, onClick }: { function MateriaCardItem({
materia: Materia, materia,
onDragStart: (e: React.DragEvent, id: string) => void, onDragStart,
isDragging: boolean, isDragging,
onClick,
}: {
materia: Materia
onDragStart: (e: React.DragEvent, id: string) => void
isDragging: boolean
onClick: () => void onClick: () => void
}) { }) {
return ( return (
<div <button
draggable draggable
onDragStart={(e) => onDragStart(e, materia.id)} onDragStart={(e) => onDragStart(e, materia.id)}
onClick={onClick} onClick={onClick}
className={`group p-3 rounded-lg border bg-white shadow-sm cursor-grab active:cursor-grabbing transition-all ${ className={`group cursor-grab rounded-lg border bg-white p-3 shadow-sm transition-all active:cursor-grabbing ${
isDragging ? 'opacity-40 scale-95' : 'hover:border-teal-400 hover:shadow-md' isDragging
? 'scale-95 opacity-40'
: 'hover:border-teal-400 hover:shadow-md'
}`} }`}
> >
<div className="flex justify-between items-start mb-1"> <div className="mb-1 flex items-start justify-between">
<span className="text-[10px] font-mono font-bold text-slate-400">{materia.clave}</span> <span className="font-mono text-[10px] font-bold text-slate-400">
<Badge variant="outline" className={`text-[9px] px-1 py-0 uppercase ${statusBadge[materia.estado] || ''}`}> {materia.clave}
</span>
<Badge
variant="outline"
className={`px-1 py-0 text-[9px] uppercase ${statusBadge[materia.estado] || ''}`}
>
{materia.estado} {materia.estado}
</Badge> </Badge>
</div> </div>
<p className="text-xs font-bold text-slate-700 leading-tight mb-1">{materia.nombre}</p> <p className="mb-1 text-xs leading-tight font-bold text-slate-700">
<div className="flex items-center justify-between mt-2"> {materia.nombre}
<span className="text-[10px] text-slate-500">{materia.creditos} CR HD:{materia.hd} HI:{materia.hi}</span> </p>
<GripVertical size={12} className="text-slate-300 opacity-0 group-hover:opacity-100 transition-opacity" /> <div className="mt-2 flex items-center justify-between">
<span className="text-[10px] text-slate-500">
{materia.creditos} CR HD:{materia.hd} HI:{materia.hi}
</span>
<GripVertical
size={12}
className="text-slate-300 opacity-0 transition-opacity group-hover:opacity-100"
/>
</div> </div>
</div> </button>
) )
} }
// --- Componente Principal --- export const Route = createFileRoute('/planes/$planId/_detalle/mapa')({
component: MapaCurricularPage,
})
function MapaCurricularPage() { function MapaCurricularPage() {
const [materias, setMaterias] = useState<Materia[]>(INITIAL_MATERIAS); const { planId } = Route.useParams() // Idealmente usa el ID de la ruta
const [lineas, setLineas] = useState<LineaCurricular[]>(INITIAL_LINEAS);
const [draggedMateria, setDraggedMateria] = useState<string | null>(null); // 1. Fetch de Datos
const [isEditModalOpen, setIsEditModalOpen] = useState(false); const { data: asignaturasApi, isLoading: loadingAsig } =
const [selectedMateria, setSelectedMateria] = useState<Materia | null>(null); usePlanAsignaturas(planId)
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId)
const ciclosTotales = 9;
const ciclosArray = Array.from({ length: ciclosTotales }, (_, i) => i + 1); // 2. Estado Local (Para interactividad)
const [materias, setMaterias] = useState<Array<Materia>>([])
const [lineas, setLineas] = useState<Array<LineaCurricular>>([])
const [draggedMateria, setDraggedMateria] = useState<string | null>(null)
const [isEditModalOpen, setIsEditModalOpen] = useState(false)
const [selectedMateria, setSelectedMateria] = useState<Materia | null>(null)
const [hasAreaComun, setHasAreaComun] = useState(false)
const [nombreNuevaLinea, setNombreNuevaLinea] = useState('') // Para el input de nombre personalizado
const manejarAgregarLinea = (nombre: string) => {
const nombreNormalizado = nombre.trim()
// Validar si es Área Común (insensible a mayúsculas/minúsculas)
const esAreaComun =
nombreNormalizado.toLowerCase() === 'área común' ||
nombreNormalizado.toLowerCase() === 'area comun'
if (esAreaComun && hasAreaComun) {
alert('El Área Común ya ha sido agregada.')
return
}
const nueva = {
id: crypto.randomUUID(),
nombre: nombreNormalizado,
orden: lineas.length + 1,
}
setLineas([...lineas, nueva])
if (esAreaComun) {
setHasAreaComun(true)
}
setNombreNuevaLinea('') // Limpiar input
}
const tieneAreaComun = useMemo(() => {
return lineas.some(
(l) =>
l.nombre.toLowerCase() === 'área común' ||
l.nombre.toLowerCase() === 'area comun',
)
}, [lineas])
// 3. Sincronizar API -> Estado Local
useEffect(() => {
if (asignaturasApi) setMaterias(mapAsignaturasToMaterias(asignaturasApi))
}, [asignaturasApi])
useEffect(() => {
if (lineasApi) setLineas(mapLineasToLineaCurricular(lineasApi))
}, [lineasApi])
const ciclosTotales = 9
const ciclosArray = Array.from({ length: ciclosTotales }, (_, i) => i + 1)
// Nuevo estado para controlar los datos temporales del modal de edición
const [editingData, setEditingData] = useState<Materia | null>(null)
// 1. FUNCION DE GUARDAR MODAL
const handleSaveChanges = () => {
if (!editingData) return
console.log(materias)
setMaterias((prev) =>
prev.map((m) => (m.id === editingData.id ? { ...editingData } : m)),
)
setIsEditModalOpen(false)
}
// 2. MODIFICACIÓN: Zona de soltado siempre visible
// Cambiamos la condición: Mostramos la sección si hay materias sin asignar
// O si simplemente queremos tener el "depósito" disponible.
const unassignedMaterias = materias.filter((m) => m.ciclo === null)
// --- Lógica de Gestión --- // --- Lógica de Gestión ---
const agregarLinea = (nombre: string) => { const agregarLinea = (nombre: string) => {
const nueva = { id: crypto.randomUUID(), nombre, orden: lineas.length + 1 }; const nueva = { id: crypto.randomUUID(), nombre, orden: lineas.length + 1 }
setLineas([...lineas, nueva]); setLineas([...lineas, nueva])
}; }
const borrarLinea = (id: string) => { const borrarLinea = (id: string) => {
setMaterias(prev => prev.map(m => m.lineaCurricularId === id ? { ...m, ciclo: null, lineaCurricularId: null } : m)); setMaterias((prev) =>
setLineas(prev => prev.filter(l => l.id !== id)); prev.map((m) =>
}; m.lineaCurricularId === id
? { ...m, ciclo: null, lineaCurricularId: null }
: m,
),
)
setLineas((prev) => prev.filter((l) => l.id !== id))
}
// --- Selectores/Cálculos ---
const getTotalesCiclo = (ciclo: number) => { const getTotalesCiclo = (ciclo: number) => {
return materias.filter(m => m.ciclo === ciclo).reduce((acc, m) => ({ return materias
cr: acc.cr + (m.creditos || 0), hd: acc.hd + (m.hd || 0), hi: acc.hi + (m.hi || 0) .filter((m) => m.ciclo === ciclo)
}), { cr: 0, hd: 0, hi: 0 }); .reduce(
}; (acc, m) => ({
cr: acc.cr + (m.creditos || 0),
hd: acc.hd + (m.hd || 0),
hi: acc.hi + (m.hi || 0),
}),
{ cr: 0, hd: 0, hi: 0 },
)
}
const getSubtotalLinea = (lineaId: string) => { const getSubtotalLinea = (lineaId: string) => {
return materias.filter(m => m.lineaCurricularId === lineaId && m.ciclo !== null).reduce((acc, m) => ({ return materias
cr: acc.cr + (m.creditos || 0), hd: acc.hd + (m.hd || 0), hi: acc.hi + (m.hi || 0) .filter((m) => m.lineaCurricularId === lineaId && m.ciclo !== null)
}), { cr: 0, hd: 0, hi: 0 }); .reduce(
}; (acc, m) => ({
cr: acc.cr + (m.creditos || 0),
hd: acc.hd + (m.hd || 0),
hi: acc.hi + (m.hi || 0),
}),
{ cr: 0, hd: 0, hi: 0 },
)
}
// --- Handlers Drag & Drop --- const handleDragStart = (e: React.DragEvent, id: string) => {
const handleDragStart = (e: React.DragEvent, id: string) => { setDraggedMateria(id); e.dataTransfer.effectAllowed = 'move'; }; setDraggedMateria(id)
const handleDragOver = (e: React.DragEvent) => e.preventDefault(); e.dataTransfer.effectAllowed = 'move'
const handleDrop = (e: React.DragEvent, ciclo: number | null, lineaId: string | null) => { }
e.preventDefault(); const handleDragOver = (e: React.DragEvent) => e.preventDefault()
const handleDrop = (
e: React.DragEvent,
ciclo: number | null,
lineaId: string | null,
) => {
e.preventDefault()
if (draggedMateria) { if (draggedMateria) {
setMaterias(prev => prev.map(m => m.id === draggedMateria ? { ...m, ciclo, lineaCurricularId: lineaId } : m)); setMaterias((prev) =>
setDraggedMateria(null); prev.map((m) =>
m.id === draggedMateria
? { ...m, ciclo, lineaCurricularId: lineaId }
: m,
),
)
setDraggedMateria(null)
} }
}; }
// --- Estadísticas Generales --- const stats = useMemo(
const stats = materias.reduce((acc, m) => { () =>
if (m.ciclo !== null) { materias.reduce(
acc.cr += m.creditos || 0; acc.hd += m.hd || 0; acc.hi += m.hi || 0; (acc, m) => {
} if (m.ciclo !== null) {
return acc; acc.cr += m.creditos || 0
}, { cr: 0, hd: 0, hi: 0 }); acc.hd += m.hd || 0
acc.hi += m.hi || 0
}
return acc
},
{ cr: 0, hd: 0, hi: 0 },
),
[materias],
)
if (loadingAsig || loadingLineas)
return <div className="p-10 text-center">Cargando mapa curricular...</div>
return ( return (
<div className="container mx-auto px-2 py-6"> <div className="container mx-auto px-2 py-6">
{/* Header */} {/* Header */}
<div className="flex justify-between items-center mb-6"> <div className="mb-6 flex items-center justify-between">
<div> <div>
<h2 className="text-xl font-bold">Mapa Curricular</h2> <h2 className="text-xl font-bold">Mapa Curricular</h2>
<p className="text-sm text-slate-500">Organiza las materias por línea curricular y ciclo</p> <p className="text-sm text-slate-500">
Organiza las materias de la petición por línea y ciclo
</p>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{materias.filter(m => !m.ciclo).length > 0 && ( {materias.filter((m) => !m.ciclo).length > 0 && (
<Badge className="bg-amber-50 text-amber-600 border-amber-100 hover:bg-amber-50"> <Badge className="border-amber-100 bg-amber-50 text-amber-600 hover:bg-amber-50">
<AlertTriangle size={14} className="mr-1" /> {materias.filter(m => !m.ciclo).length} materias sin asignar <AlertTriangle size={14} className="mr-1" />{' '}
{materias.filter((m) => !m.ciclo).length} sin asignar
</Badge> </Badge>
)} )}
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button className="bg-teal-700 hover:bg-teal-800 text-white"> <Button className="bg-teal-700 text-white hover:bg-teal-800">
<Plus size={16} className="mr-2" /> Agregar <ChevronDown size={14} className="ml-2" /> <Plus size={16} className="mr-2" /> Agregar{' '}
<ChevronDown size={14} className="ml-2" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => agregarLinea("Nueva Línea")}>Nueva Línea Curricular</DropdownMenuItem> {!tieneAreaComun && (
<DropdownMenuItem onClick={() => agregarLinea("Área Común")}>Agregar Área Común</DropdownMenuItem> <>
<DropdownMenuItem
onClick={() => manejarAgregarLinea('Área Común')}
className="font-bold text-teal-700"
>
+ Agregar Área Común
</DropdownMenuItem>
<div className="my-1 border-t border-slate-100" />
</>
)}
{/* Input para nombre personalizado */}
<div className="p-2">
<label className="text-[10px] font-bold text-slate-400 uppercase">
Nombre de Línea
</label>
<div className="mt-1 flex gap-1">
<Input
value={nombreNuevaLinea}
onChange={(e) => setNombreNuevaLinea(e.target.value)}
placeholder="Ej: Optativas"
className="h-8 text-xs"
/>
<Button
size="sm"
className="h-8 px-2"
onClick={() => manejarAgregarLinea(nombreNuevaLinea)}
disabled={!nombreNuevaLinea.trim()}
>
<Plus size={14} />
</Button>
</div>
</div>
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
</div> </div>
{/* Barra Totales */} {/* Barra Totales */}
<div className="bg-slate-50/80 border border-slate-200 rounded-xl p-4 mb-8 flex gap-10"> <div className="mb-8 flex gap-10 rounded-xl border border-slate-200 bg-slate-50/80 p-4">
<StatItem label="Total Créditos" value={stats.cr} total={320} /> <StatItem label="Total Créditos" value={stats.cr} total={320} />
<StatItem label="Total HD" value={stats.hd} /> <StatItem label="Total HD" value={stats.hd} />
<StatItem label="Total HI" value={stats.hi} /> <StatItem label="Total HI" value={stats.hi} />
<StatItem label="Total Horas" value={stats.hd + stats.hi} /> <StatItem label="Total Horas" value={stats.hd + stats.hi} />
</div> </div>
{/* Grid Principal */}
<div className="overflow-x-auto pb-6"> <div className="overflow-x-auto pb-6">
<div className="min-w-[1500px]"> <div className="min-w-[1500px]">
{/* Header Ciclos */} <div
<div className="grid gap-3 mb-4" style={{ gridTemplateColumns: `220px repeat(${ciclosTotales}, 1fr) 120px` }}> className="mb-4 grid gap-3"
<div className="text-xs font-bold text-slate-400 self-end px-2">LÍNEA CURRICULAR</div> style={{
{ciclosArray.map(n => <div key={n} className="bg-slate-100 rounded-lg p-2 text-center text-sm font-bold text-slate-600">Ciclo {n}</div>)} gridTemplateColumns: `220px repeat(${ciclosTotales}, 1fr) 120px`,
<div className="text-xs font-bold text-slate-400 self-end text-center">SUBTOTAL</div> }}
>
<div className="self-end px-2 text-xs font-bold text-slate-400">
LÍNEA CURRICULAR
</div>
{ciclosArray.map((n) => (
<div
key={n}
className="rounded-lg bg-slate-100 p-2 text-center text-sm font-bold text-slate-600"
>
Ciclo {n}
</div>
))}
<div className="self-end text-center text-xs font-bold text-slate-400">
SUBTOTAL
</div>
</div> </div>
{/* Filas por Línea */}
{lineas.map((linea, idx) => { {lineas.map((linea, idx) => {
const sub = getSubtotalLinea(linea.id); const sub = getSubtotalLinea(linea.id)
return ( return (
<div key={linea.id} className="grid gap-3 mb-3" style={{ gridTemplateColumns: `220px repeat(${ciclosTotales}, 1fr) 120px` }}> <div
<div className={`p-4 rounded-xl border-l-4 flex justify-between items-center ${lineColors[idx % lineColors.length]}`}> key={linea.id}
className="mb-3 grid gap-3"
style={{
gridTemplateColumns: `220px repeat(${ciclosTotales}, 1fr) 120px`,
}}
>
<div
className={`flex items-center justify-between rounded-xl border-l-4 p-4 ${lineColors[idx % lineColors.length]}`}
>
<span className="text-xs font-bold">{linea.nombre}</span> <span className="text-xs font-bold">{linea.nombre}</span>
<Trash2 size={14} className="text-slate-400 hover:text-red-500 cursor-pointer" onClick={() => borrarLinea(linea.id)} /> <Trash2
size={14}
className="cursor-pointer text-slate-400 hover:text-red-500"
onClick={() => borrarLinea(linea.id)}
/>
</div> </div>
{ciclosArray.map(ciclo => ( {ciclosArray.map((ciclo) => (
<div <div
key={ciclo} key={ciclo}
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, ciclo, linea.id)} onDrop={(e) => handleDrop(e, ciclo, linea.id)}
className="min-h-[140px] p-2 rounded-xl border-2 border-dashed border-slate-100 bg-slate-50/20 space-y-2" className="min-h-[140px] space-y-2 rounded-xl border-2 border-dashed border-slate-100 bg-slate-50/20 p-2"
> >
{materias.filter(m => m.ciclo === ciclo && m.lineaCurricularId === linea.id).map(m => ( {materias
<MateriaCardItem key={m.id} materia={m} isDragging={draggedMateria === m.id} onDragStart={handleDragStart} onClick={() => { setSelectedMateria(m); setIsEditModalOpen(true); }} /> .filter(
))} (m) =>
m.ciclo === ciclo && m.lineaCurricularId === linea.id,
)
.map((m) => (
<MateriaCardItem
key={m.id}
materia={m}
isDragging={draggedMateria === m.id}
onDragStart={handleDragStart}
onClick={() => {
setEditingData(m)
setIsEditModalOpen(true)
}}
/>
))}
</div> </div>
))} ))}
<div className="p-4 bg-slate-50 rounded-xl flex flex-col justify-center text-[10px] text-slate-500 font-medium border border-slate-100"> <div className="flex flex-col justify-center rounded-xl border border-slate-100 bg-slate-50 p-4 text-[10px] font-medium text-slate-500">
<div>Cr: {sub.cr}</div><div>HD: {sub.hd}</div><div>HI: {sub.hi}</div> <div>Cr: {sub.cr}</div>
<div>HD: {sub.hd}</div>
<div>HI: {sub.hi}</div>
</div> </div>
</div> </div>
) )
})} })}
{/* Fila Totales Ciclo */} <div
<div className="grid gap-3 mt-6 border-t pt-4" style={{ gridTemplateColumns: `220px repeat(${ciclosTotales}, 1fr) 120px` }}> className="mt-6 grid gap-3 border-t pt-4"
<div className="p-2 font-bold text-slate-600">Totales por Ciclo</div> style={{
{ciclosArray.map(ciclo => { gridTemplateColumns: `220px repeat(${ciclosTotales}, 1fr) 120px`,
const t = getTotalesCiclo(ciclo); }}
>
<div className="p-2 font-bold text-slate-600">
Totales por Ciclo
</div>
{ciclosArray.map((ciclo) => {
const t = getTotalesCiclo(ciclo)
return ( return (
<div key={ciclo} className="text-[10px] text-center p-2 bg-slate-50 rounded-lg"> <div
key={ciclo}
className="rounded-lg bg-slate-50 p-2 text-center text-[10px]"
>
<div className="font-bold text-slate-700">Cr: {t.cr}</div> <div className="font-bold text-slate-700">Cr: {t.cr}</div>
<div>HD: {t.hd} HI: {t.hi}</div> <div>
HD: {t.hd} HI: {t.hi}
</div>
</div> </div>
) )
})} })}
<div className="bg-teal-50 rounded-lg p-2 text-center text-teal-800 font-bold text-xs flex flex-col justify-center"> <div className="flex flex-col justify-center rounded-lg bg-teal-50 p-2 text-center text-xs font-bold text-teal-800">
<div>{stats.cr} Cr</div><div>{stats.hd + stats.hi} Hrs</div> <div>{stats.cr} Cr</div>
<div>{stats.hd + stats.hi} Hrs</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{/* Modal de Edición */} {/* Materias Sin Asignar */}
<Dialog open={isEditModalOpen} onOpenChange={setIsEditModalOpen}> {/* SECCIÓN DE MATERIAS SIN ASIGNAR (Mejorada para estar siempre disponible) */}
<DialogContent className="sm:max-w-[500px]"> <div className="mt-10 rounded-2xl border border-slate-200 bg-slate-50 p-6">
<DialogHeader><DialogTitle>Editar Materia</DialogTitle></DialogHeader> <div className="mb-4 flex items-center justify-between">
{selectedMateria && ( <div className="flex items-center gap-2 text-slate-600">
<div className="grid grid-cols-2 gap-4 py-4"> <h3 className="text-sm font-bold tracking-wider uppercase">
<div className="space-y-2"><label className="text-xs font-bold uppercase">Clave</label><Input defaultValue={selectedMateria.clave} /></div> Bandeja de Entrada / Materias sin asignar
<div className="space-y-2"><label className="text-xs font-bold uppercase">Nombre</label><Input defaultValue={selectedMateria.nombre} /></div>
<div className="space-y-2"><label className="text-xs font-bold uppercase">Créditos</label><Input type="number" defaultValue={selectedMateria.creditos} /></div>
<div className="flex gap-2">
<div className="space-y-2"><label className="text-xs font-bold uppercase">HD</label><Input type="number" defaultValue={selectedMateria.hd} /></div>
<div className="space-y-2"><label className="text-xs font-bold uppercase">HI</label><Input type="number" defaultValue={selectedMateria.hi} /></div>
</div>
</div>
)}
<div className="flex justify-end gap-3 mt-4">
<Button variant="outline" onClick={() => setIsEditModalOpen(false)}>Cancelar</Button>
<Button className="bg-teal-700 text-white">Guardar Cambios</Button>
</div>
</DialogContent>
</Dialog>
{/* 4. Materias Pendientes (Sin Asignar) */}
{materias.filter(m => m.ciclo === null).length > 0 && (
<div className="mt-10 p-6 bg-slate-50 rounded-2xl border border-slate-200 shadow-sm animate-in slide-in-from-bottom-4 duration-500">
<div className="flex items-center gap-2 mb-4 text-amber-600">
<AlertTriangle size={20} />
<h3 className="font-bold text-sm uppercase tracking-tight">
Materias pendientes de asignar ({materias.filter(m => m.ciclo === null).length})
</h3> </h3>
<Badge variant="secondary">{unassignedMaterias.length}</Badge>
</div> </div>
<p className="text-xs text-slate-400">
<div Arrastra una materia aquí para quitarla del mapa
className={`flex flex-wrap gap-4 min-h-[100px] p-4 rounded-xl border-2 border-dashed transition-all ${
draggedMateria ? 'border-amber-200 bg-amber-50/50' : 'border-slate-200 bg-white/50'
}`}
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, null, null)} // null devuelve la materia al estado "sin asignar"
>
{materias
.filter(m => m.ciclo === null)
.map(m => (
<div key={m.id} className="w-[200px]">
<MateriaCardItem
materia={m}
isDragging={draggedMateria === m.id}
onDragStart={handleDragStart}
onClick={() => { setSelectedMateria(m); setIsEditModalOpen(true); }}
/>
</div>
))}
</div>
<p className="mt-3 text-[11px] text-slate-400 italic text-center">
Arrastra las materias desde aquí hacia cualquier ciclo y línea del mapa curricular.
</p> </p>
</div> </div>
)}
<div
className={`flex min-h-[120px] flex-wrap gap-4 rounded-xl border-2 border-dashed p-4 transition-colors ${
draggedMateria
? 'border-teal-300 bg-teal-50/50'
: 'border-slate-200 bg-white/50'
}`}
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, null, null)} // Limpia ciclo y línea
>
{unassignedMaterias.map((m) => (
<div key={m.id} className="w-[200px]">
<MateriaCardItem
materia={m}
isDragging={draggedMateria === m.id}
onDragStart={handleDragStart}
onClick={() => {
setEditingData(m) // Cargamos los datos en el estado de edición
setIsEditModalOpen(true)
}}
/>
</div>
))}
{unassignedMaterias.length === 0 && (
<div className="flex w-full items-center justify-center text-sm text-slate-400">
No hay materias pendientes. Arrastra una materia aquí para
desasignarla.
</div>
)}
</div>
</div>
{/* Modal de Edición */}
<Dialog open={isEditModalOpen} onOpenChange={setIsEditModalOpen}>
<DialogContent className="sm:max-w-[550px]">
<DialogHeader>
<DialogTitle className="font-bold text-slate-700">
Editar Materia
</DialogTitle>
</DialogHeader>
{/* Verificación de seguridad: solo renderiza si hay datos */}
{editingData ? (
<div className="grid gap-4 py-4">
{/* Fila 1: Clave y Nombre */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase">
Clave
</label>
<Input
value={editingData.clave}
onChange={(e) =>
setEditingData({ ...editingData, clave: e.target.value })
}
/>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase">
Nombre
</label>
<Input
value={editingData.nombre}
onChange={(e) =>
setEditingData({ ...editingData, nombre: e.target.value })
}
/>
</div>
</div>
{/* Fila 2: Créditos y Horas */}
<div className="grid grid-cols-3 gap-4">
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase">
Créditos
</label>
<Input type="number" value={editingData.creditos} />
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase">
HD (Horas Docente)
</label>
<Input type="number" value={editingData.hd} />
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase">
HI (Horas Indep.)
</label>
<Input type="number" value={editingData.hi} />
</div>
</div>
{/* Fila 3: Ciclo y Línea */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase">
Ciclo
</label>
<Select value={editingData.ciclo?.toString() || 'null'}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{ciclosArray.map((n) => (
<SelectItem key={n} value={n.toString()}>
Ciclo {n}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase">
Línea Curricular
</label>
<Select value={editingData.lineaCurricularId || 'null'}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{lineas.map((l) => (
<SelectItem key={l.id} value={l.id}>
{l.nombre}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
{/* Fila 4: Seriación (Igual a tu imagen) */}
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase">
Seriación (Prerrequisitos)
</label>
<Select>
<SelectTrigger>
<SelectValue placeholder="Seleccionar materia..." />
</SelectTrigger>
<SelectContent>
{materias.map((m) => (
<SelectItem key={m.id} value={m.clave}>
{m.nombre}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="mt-2 flex gap-2">
{/* Aquí usamos el array vacío que inicializamos en el mapeador */}
{editingData.prerrequisitos.map((pre) => (
<Badge
key={pre}
variant="secondary"
className="bg-slate-100 text-slate-600"
>
{pre} <span className="ml-1 cursor-pointer">×</span>
</Badge>
))}
</div>
</div>
{/* Fila 5: Tipo */}
<div className="space-y-2">
<label className="text-xs font-bold text-slate-500 uppercase">
Tipo
</label>
<Select value={editingData.tipo}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="obligatoria">Obligatoria</SelectItem>
<SelectItem value="optativa">Optativa</SelectItem>
</SelectContent>
</Select>
</div>
<div className="mt-4 flex justify-end gap-3">
<Button
variant="outline"
onClick={() => setIsEditModalOpen(false)}
>
Cancelar
</Button>
<Button
className="bg-teal-700 text-white"
onClick={handleSaveChanges}
>
Guardar
</Button>
</div>
</div>
) : (
<div className="py-20 text-center">No hay datos seleccionados</div>
)}
</DialogContent>
</Dialog>
</div> </div>
) )
} }

View File

@@ -1,9 +1,27 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useState } from 'react' import {
Plus,
Copy,
Search,
Filter,
ChevronRight,
BookOpen,
Loader2,
} from 'lucide-react'
import { useState, useMemo } from 'react'
import type { Materia } from '@/types/plan'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge' import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { import {
Table, Table,
TableBody, TableBody,
@@ -12,204 +30,257 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from '@/components/ui/table' } from '@/components/ui/table'
import { usePlanAsignaturas, usePlanLineas } from '@/data'
export const Route = createFileRoute('/planes/$planId/_detalle/materias')({ // --- Configuración de Estilos ---
component: Materias, const statusConfig: Record<string, { label: string; className: string }> = {
}) borrador: { label: 'Borrador', className: 'bg-slate-100 text-slate-600' },
revisada: { label: 'Revisada', className: 'bg-amber-100 text-amber-700' },
type Materia = { aprobada: { label: 'Aprobada', className: 'bg-emerald-100 text-emerald-700' },
id: string;
clave: string
nombre: string
creditos: number
hd: number
hi: number
ciclo: string
linea: string
tipo: 'Obligatoria' | 'Optativa' | 'Troncal'
estado: 'Aprobada' | 'Revisada' | 'Borrador'
} }
const MATERIAS: Materia[] = [ const tipoConfig: Record<string, { label: string; className: string }> = {
{ obligatoria: { label: 'Obligatoria', className: 'bg-blue-100 text-blue-700' },
id: "1", optativa: { label: 'Optativa', className: 'bg-purple-100 text-purple-700' },
clave: 'MAT101', troncal: { label: 'Troncal', className: 'bg-slate-100 text-slate-700' },
nombre: 'Cálculo Diferencial', }
creditos: 8,
hd: 4,
hi: 4,
ciclo: 'Ciclo 1',
linea: 'Formación Básica',
tipo: 'Obligatoria',
estado: 'Aprobada',
},
{
id: "2",
clave: 'FIS101',
nombre: 'Física Mecánica',
creditos: 6,
hd: 3,
hi: 3,
ciclo: 'Ciclo 1',
linea: 'Formación Básica',
tipo: 'Obligatoria',
estado: 'Aprobada',
},
{
id: "3",
clave: 'PRO101',
nombre: 'Fundamentos de Programación',
creditos: 8,
hd: 4,
hi: 4,
ciclo: 'Ciclo 1',
linea: 'Ciencias de la Computación',
tipo: 'Obligatoria',
estado: 'Revisada',
},
{
id: "4",
clave: 'EST101',
nombre: 'Estructura de Datos',
creditos: 6,
hd: 3,
hi: 3,
ciclo: 'Ciclo 2',
linea: 'Ciencias de la Computación',
tipo: 'Obligatoria',
estado: 'Borrador',
},
]
function Materias() { // --- Mapeadores de API ---
const [search, setSearch] = useState('') const mapAsignaturas = (asigApi: Array<any> = []): Array<Materia> => {
const [filtro, setFiltro] = useState<'Todas' | Materia['tipo']>('Todas') return asigApi.map((asig) => ({
id: asig.id,
clave: asig.codigo,
nombre: asig.nombre,
creditos: asig.creditos ?? 0,
ciclo: asig.numero_ciclo ?? null,
lineaCurricularId: asig.linea_plan_id ?? null,
tipo:
asig.tipo?.toLowerCase() === 'obligatoria' ? 'obligatoria' : 'optativa',
estado: 'borrador', // O el campo que venga de tu API
hd: Math.floor((asig.horas_semana ?? 0) / 2),
hi: Math.ceil((asig.horas_semana ?? 0) / 2),
}))
}
const materiasFiltradas = MATERIAS.filter((m) => { export const Route = createFileRoute('/planes/$planId/_detalle/materias')({
const okFiltro = filtro === 'Todas' || m.tipo === filtro component: MateriasPage,
const okSearch = })
m.nombre.toLowerCase().includes(search.toLowerCase()) ||
m.clave.toLowerCase().includes(search.toLowerCase())
return okFiltro && okSearch function MateriasPage() {
const { planId } = Route.useParams()
const navigate = useNavigate()
// 1. Fetch de datos reales
const { data: asignaturasApi, isLoading: loadingAsig } =
usePlanAsignaturas(planId)
const { data: lineasApi, isLoading: loadingLineas } = usePlanLineas(planId)
// 2. Estados de filtrado
const [searchTerm, setSearchTerm] = useState('')
const [filterTipo, setFilterTipo] = useState<string>('all')
const [filterEstado, setFilterEstado] = useState<string>('all')
const [filterLinea, setFilterLinea] = useState<string>('all')
// 3. Procesamiento de datos
const materias = useMemo(
() => mapAsignaturas(asignaturasApi),
[asignaturasApi],
)
const lineas = useMemo(() => lineasApi || [], [lineasApi])
const filteredMaterias = materias.filter((m) => {
const matchesSearch =
m.nombre.toLowerCase().includes(searchTerm.toLowerCase()) ||
m.clave.toLowerCase().includes(searchTerm.toLowerCase())
const matchesTipo = filterTipo === 'all' || m.tipo === filterTipo
const matchesEstado = filterEstado === 'all' || m.estado === filterEstado
const matchesLinea =
filterLinea === 'all' || m.lineaCurricularId === filterLinea
return matchesSearch && matchesTipo && matchesEstado && matchesLinea
}) })
const totalCreditos = materiasFiltradas.reduce( const getLineaNombre = (lineaId: string | null) => {
(acc, m) => acc + m.creditos, if (!lineaId) return 'Sin asignar'
0 return lineas.find((l: any) => l.id === lineaId)?.nombre || 'Desconocida'
) }
if (loadingAsig || loadingLineas) {
return (
<div className="flex h-64 items-center justify-center">
<Loader2 className="text-muted-foreground h-8 w-8 animate-spin" />
</div>
)
}
return ( return (
<div className="space-y-6"> <div className="container mx-auto space-y-6 px-6 py-6">
{/* Header */} {/* Header */}
<div className="flex justify-between items-start"> <div className="flex flex-wrap items-center justify-between gap-4">
<div> <div>
<h2 className="text-xl font-semibold">Materias del Plan</h2> <h2 className="text-foreground text-xl font-bold">
<p className="text-sm text-muted-foreground"> Materias del Plan
{materiasFiltradas.length} materias · {totalCreditos} créditos </h2>
<p className="text-muted-foreground mt-1 text-sm">
{materias.length} materias en total {filteredMaterias.length}{' '}
filtradas
</p> </p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="outline">Clonar de mi Facultad</Button> <Button variant="outline" size="sm">
<Button variant="outline">Clonar de otra Facultad</Button> <Copy className="mr-2 h-4 w-4" /> Clonar
</Button>
<Button className="bg-emerald-700 hover:bg-emerald-800"> <Button className="bg-emerald-700 hover:bg-emerald-800">
+ Nueva Materia <Plus className="mr-2 h-4 w-4" /> Nueva Materia
</Button> </Button>
</div> </div>
</div> </div>
{/* Buscador y filtros */} {/* Barra de Filtros Avanzada */}
<div className="flex items-center gap-4"> <div className="flex flex-wrap items-center gap-3 rounded-xl border bg-slate-50 p-4">
<Input <div className="relative min-w-[240px] flex-1">
placeholder="Buscar por nombre o clave..." <Search className="text-muted-foreground absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2" />
value={search} <Input
onChange={(e) => setSearch(e.target.value)} placeholder="Buscar por nombre o clave..."
className="w-64" value={searchTerm}
/> onChange={(e) => setSearchTerm(e.target.value)}
className="bg-white pl-9"
/>
</div>
<div className="flex gap-2"> <div className="flex flex-wrap items-center gap-2">
{['Todas', 'Obligatoria', 'Optativa', 'Troncal'].map((t) => ( <Filter className="text-muted-foreground mr-1 h-4 w-4" />
<Button
key={t} <Select value={filterTipo} onValueChange={setFilterTipo}>
variant={filtro === t ? 'secondary' : 'ghost'} <SelectTrigger className="w-[140px] bg-white">
size="sm" <SelectValue placeholder="Tipo" />
onClick={() => setFiltro(t as any)} </SelectTrigger>
> <SelectContent>
{t === 'Obligatoria' ? 'Obligatorias' : t} <SelectItem value="all">Todos los tipos</SelectItem>
</Button> <SelectItem value="obligatoria">Obligatoria</SelectItem>
))} <SelectItem value="optativa">Optativa</SelectItem>
</SelectContent>
</Select>
<Select value={filterEstado} onValueChange={setFilterEstado}>
<SelectTrigger className="w-[140px] bg-white">
<SelectValue placeholder="Estado" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todos los estados</SelectItem>
<SelectItem value="borrador">Borrador</SelectItem>
<SelectItem value="revisada">Revisada</SelectItem>
<SelectItem value="aprobada">Aprobada</SelectItem>
</SelectContent>
</Select>
<Select value={filterLinea} onValueChange={setFilterLinea}>
<SelectTrigger className="w-[180px] bg-white">
<SelectValue placeholder="Línea" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Todas las líneas</SelectItem>
{lineas.map((linea: any) => (
<SelectItem key={linea.id} value={linea.id}>
{linea.nombre}
</SelectItem>
))}
</SelectContent>
</Select>
</div> </div>
</div> </div>
{/* Tabla */} {/* Tabla Pro */}
<div className="rounded-md border"> <div className="overflow-hidden rounded-xl border bg-white shadow-sm">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow className="bg-slate-50/50">
<TableHead>Clave</TableHead> <TableHead className="w-[120px]">Clave</TableHead>
<TableHead>Nombre</TableHead> <TableHead>Nombre</TableHead>
<TableHead className="text-center">Créditos</TableHead> <TableHead className="text-center">Créditos</TableHead>
<TableHead className="text-center">HD</TableHead> <TableHead className="text-center">Ciclo</TableHead>
<TableHead className="text-center">HI</TableHead> <TableHead>Línea Curricular</TableHead>
<TableHead>Ciclo</TableHead>
<TableHead>Línea</TableHead>
<TableHead>Tipo</TableHead> <TableHead>Tipo</TableHead>
<TableHead>Estado</TableHead> <TableHead>Estado</TableHead>
<TableHead className="text-center">Acciones</TableHead> <TableHead className="w-[50px]"></TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{materiasFiltradas.map((m) => ( {filteredMaterias.length === 0 ? (
<TableRow key={m.clave}> <TableRow>
<TableCell className="text-muted-foreground"> <TableCell colSpan={8} className="h-40 text-center">
{m.clave} <div className="text-muted-foreground flex flex-col items-center justify-center">
<BookOpen className="mb-2 h-10 w-10 opacity-20" />
<p className="font-medium">No se encontraron materias</p>
<p className="text-xs">
Intenta cambiar los filtros de búsqueda
</p>
</div>
</TableCell> </TableCell>
<TableCell className="font-medium">{m.nombre}</TableCell> </TableRow>
<TableCell className="text-center">{m.creditos}</TableCell> ) : (
<TableCell className="text-center">{m.hd}</TableCell> filteredMaterias.map((materia) => (
<TableCell className="text-center">{m.hi}</TableCell> <TableRow
<TableCell>{m.ciclo}</TableCell> key={materia.id}
<TableCell>{m.linea}</TableCell> className="group cursor-pointer transition-colors hover:bg-slate-50/80"
onClick={() =>
<TableCell> navigate({
<Badge variant="secondary">{m.tipo}</Badge> to: '/planes/$planId/asignaturas/$asignaturaId',
</TableCell> params: {
planId,
<TableCell> asignaturaId: materia.id, // 👈 puede ser índice, consecutivo o slug
<Badge },
variant="secondary" state: {
className={ realId: materia.id, // 👈 ID largo oculto
m.estado === 'Aprobada' } as any,
? 'bg-emerald-100 text-emerald-700' })
: m.estado === 'Revisada'
? 'bg-blue-100 text-blue-700'
: 'bg-gray-100 text-gray-500'
} }
> >
{m.estado} <TableCell className="font-mono text-xs font-bold text-slate-400">
</Badge> {materia.clave}
</TableCell>
</TableCell> <TableCell className="font-semibold text-slate-700">
{materia.nombre}
<TableCell className="text-center"> </TableCell>
<Button variant="ghost" size="icon"> <TableCell className="text-center font-medium">
{materia.creditos}
</Button> </TableCell>
</TableCell> <TableCell className="text-center">
</TableRow> {materia.ciclo ? (
))} <Badge variant="outline" className="font-normal">
Ciclo {materia.ciclo}
{materiasFiltradas.length === 0 && ( </Badge>
<TableRow> ) : (
<TableCell <span className="text-slate-300"></span>
colSpan={10} )}
className="text-center py-6 text-muted-foreground" </TableCell>
> <TableCell className="text-sm text-slate-600">
No se encontraron materias {getLineaNombre(materia.lineaCurricularId)}
</TableCell> </TableCell>
</TableRow> <TableCell>
<Badge
variant="outline"
className={`capitalize shadow-sm ${tipoConfig[materia.tipo]?.className}`}
>
{tipoConfig[materia.tipo]?.label}
</Badge>
</TableCell>
<TableCell>
<Badge
variant="outline"
className={`capitalize shadow-sm ${statusConfig[materia.estado]?.className}`}
>
{statusConfig[materia.estado]?.label}
</Badge>
</TableCell>
<TableCell>
<div className="opacity-0 transition-opacity group-hover:opacity-100">
<ChevronRight className="h-5 w-5 text-slate-400" />
</div>
</TableCell>
</TableRow>
))
)} )}
</TableBody> </TableBody>
</Table> </Table>

View File

@@ -1,6 +1,16 @@
import { createFileRoute, Outlet, Link } from '@tanstack/react-router' import { createFileRoute, Outlet, Link } from '@tanstack/react-router'
import { ChevronLeft, GraduationCap, Clock, Hash, CalendarDays, Rocket, BookOpen, CheckCircle2 } from "lucide-react" import {
import { Badge } from "@/components/ui/badge" ChevronLeft,
GraduationCap,
Clock,
Hash,
CalendarDays,
Rocket,
BookOpen,
CheckCircle2,
} from 'lucide-react'
import { Badge } from '@/components/ui/badge'
export const Route = createFileRoute('/planes/$planId/_detalle')({ export const Route = createFileRoute('/planes/$planId/_detalle')({
component: RouteComponent, component: RouteComponent,
@@ -12,11 +22,11 @@ function RouteComponent() {
return ( return (
<div className="min-h-screen bg-white"> <div className="min-h-screen bg-white">
{/* 1. Header Superior con Sombra (Volver a planes) */} {/* 1. Header Superior con Sombra (Volver a planes) */}
<div className="border-b bg-white/50 backdrop-blur-sm sticky top-0 z-20 shadow-sm"> <div className="sticky top-0 z-20 border-b bg-white/50 shadow-sm backdrop-blur-sm">
<div className="px-6 py-2"> <div className="px-6 py-2">
<Link <Link
to="/planes" to="/planes"
className="flex items-center gap-1 text-xs text-gray-500 hover:text-gray-800 transition-colors w-fit" className="flex w-fit items-center gap-1 text-xs text-gray-500 transition-colors hover:text-gray-800"
> >
<ChevronLeft size={14} /> Volver a planes <ChevronLeft size={14} /> Volver a planes
</Link> </Link>
@@ -24,54 +34,91 @@ function RouteComponent() {
</div> </div>
{/* 2. Contenido Principal con Padding */} {/* 2. Contenido Principal con Padding */}
<div className="p-8 max-w-[1600px] mx-auto space-y-8"> <div className="mx-auto max-w-[1600px] space-y-8 p-8">
{/* Header del Plan y Badges */} {/* Header del Plan y Badges */}
<div className="flex flex-col md:flex-row justify-between items-start gap-4"> <div className="flex flex-col items-start justify-between gap-4 md:flex-row">
<div> <div>
<h1 className="text-3xl font-bold tracking-tight text-slate-900">Plan de Estudios 2024</h1> <h1 className="text-3xl font-bold tracking-tight text-slate-900">
<p className="text-lg text-slate-500 font-medium mt-1"> Plan de Estudios 2024
</h1>
<p className="mt-1 text-lg font-medium text-slate-500">
Ingeniería en Sistemas Computacionales Ingeniería en Sistemas Computacionales
</p> </p>
</div> </div>
{/* Badges de la derecha */} {/* Badges de la derecha */}
<div className="flex gap-2"> <div className="flex gap-2">
<Badge variant="secondary" className="bg-blue-50 text-blue-700 border-blue-100 gap-1 px-3"> <Badge
variant="secondary"
className="gap-1 border-blue-100 bg-blue-50 px-3 text-blue-700"
>
<Rocket size={12} /> Ingeniería <Rocket size={12} /> Ingeniería
</Badge> </Badge>
<Badge variant="secondary" className="bg-orange-50 text-orange-700 border-orange-100 gap-1 px-3"> <Badge
variant="secondary"
className="gap-1 border-orange-100 bg-orange-50 px-3 text-orange-700"
>
<BookOpen size={12} /> Licenciatura <BookOpen size={12} /> Licenciatura
</Badge> </Badge>
<Badge className="bg-teal-50 text-teal-700 border-teal-200 gap-1 px-3 hover:bg-teal-100"> <Badge className="gap-1 border-teal-200 bg-teal-50 px-3 text-teal-700 hover:bg-teal-100">
<CheckCircle2 size={12} /> En Revisión <CheckCircle2 size={12} /> En Revisión
</Badge> </Badge>
</div> </div>
</div> </div>
{/* 3. Cards de Información (Nivel, Duración, etc.) */} {/* 3. Cards de Información (Nivel, Duración, etc.) */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="grid grid-cols-2 gap-4 md:grid-cols-4">
<InfoCard icon={<GraduationCap className="text-slate-400" />} label="Nivel" value="Superior" /> <InfoCard
<InfoCard icon={<Clock className="text-slate-400" />} label="Duración" value="9 Semestres" /> icon={<GraduationCap className="text-slate-400" />}
<InfoCard icon={<Hash className="text-slate-400" />} label="Créditos" value="320" /> label="Nivel"
<InfoCard icon={<CalendarDays className="text-slate-400" />} label="Creación" value="14 ene 2024" /> value="Superior"
/>
<InfoCard
icon={<Clock className="text-slate-400" />}
label="Duración"
value="9 Semestres"
/>
<InfoCard
icon={<Hash className="text-slate-400" />}
label="Créditos"
value="320"
/>
<InfoCard
icon={<CalendarDays className="text-slate-400" />}
label="Creación"
value="14 ene 2024"
/>
</div> </div>
{/* 4. Navegación de Tabs */} {/* 4. Navegación de Tabs */}
<div className="border-b overflow-x-auto scrollbar-hide"> <div className="scrollbar-hide overflow-x-auto border-b">
<nav className="flex gap-8 min-w-max"> <nav className="flex min-w-max gap-8">
<Tab to="/planes/$planId/datos" params={{ planId }}>Datos Generales</Tab> <Tab to="/planes/$planId/datos" params={{ planId }}>
<Tab to="/planes/$planId/mapa" params={{ planId }}>Mapa Curricular</Tab> Datos Generales
<Tab to="/planes/$planId/materias" params={{ planId }}>Materias</Tab> </Tab>
<Tab to="/planes/$planId/flujo" params={{ planId }}>Flujo y Estados</Tab> <Tab to="/planes/$planId/mapa" params={{ planId }}>
<Tab to="/planes/$planId/iaplan" params={{ planId }}>IA del Plan</Tab> Mapa Curricular
<Tab to="/planes/$planId/documento" params={{ planId }}>Documento</Tab> </Tab>
<Tab to="/planes/$planId/historial" params={{ planId }}>Historial</Tab> <Tab to="/planes/$planId/materias" params={{ planId }}>
Materias
</Tab>
<Tab to="/planes/$planId/flujo" params={{ planId }}>
Flujo y Estados
</Tab>
<Tab to="/planes/$planId/iaplan" params={{ planId }}>
IA del Plan
</Tab>
<Tab to="/planes/$planId/documento" params={{ planId }}>
Documento
</Tab>
<Tab to="/planes/$planId/historial" params={{ planId }}>
Historial
</Tab>
</nav> </nav>
</div> </div>
{/* 5. Contenido del Tab */} {/* 5. Contenido del Tab */}
<main className="pt-2 animate-in fade-in duration-500"> <main className="animate-in fade-in pt-2 duration-500">
<Outlet /> <Outlet />
</main> </main>
</div> </div>
@@ -79,36 +126,43 @@ function RouteComponent() {
) )
} }
// Sub-componente para las tarjetas de información // Sub-componente para las tarjetas de información
function InfoCard({ icon, label, value }: { icon: React.ReactNode, label: string, value: string }) { function InfoCard({
icon,
label,
value,
}: {
icon: React.ReactNode
label: string
value: string
}) {
return ( return (
<div className="flex items-center gap-4 bg-slate-50/50 border border-slate-200/60 p-4 rounded-xl shadow-sm"> <div className="flex items-center gap-4 rounded-xl border border-slate-200/60 bg-slate-50/50 p-4 shadow-sm">
<div className="p-2 bg-white rounded-lg border shadow-sm"> <div className="rounded-lg border bg-white p-2 shadow-sm">{icon}</div>
{icon}
</div>
<div> <div>
<p className="text-[10px] uppercase font-bold text-slate-400 tracking-wider leading-none mb-1">{label}</p> <p className="mb-1 text-[10px] leading-none font-bold tracking-wider text-slate-400 uppercase">
{label}
</p>
<p className="text-sm font-semibold text-slate-700">{value}</p> <p className="text-sm font-semibold text-slate-700">{value}</p>
</div> </div>
</div> </div>
) )
} }
function Tab({ function Tab({
to, to,
params, params,
children children,
}: { }: {
to: string; to: string
params?: any; params?: any
children: React.ReactNode children: React.ReactNode
}) { }) {
return ( return (
<Link <Link
to={to} to={to}
params={params} params={params}
className="pb-3 text-sm font-medium text-slate-500 border-b-2 border-transparent hover:text-slate-800 transition-all" className="border-b-2 border-transparent pb-3 text-sm font-medium text-slate-500 transition-all hover:text-slate-800"
activeProps={{ activeProps={{
className: 'border-teal-600 text-teal-700 font-bold', className: 'border-teal-600 text-teal-700 font-bold',
}} }}
@@ -116,4 +170,4 @@ function Tab({
{children} {children}
</Link> </Link>
) )
} }

View File

@@ -1,146 +1,10 @@
import { createFileRoute } from '@tanstack/react-router' import { createFileRoute, redirect } from '@tanstack/react-router'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import {
BookOpen,
Sparkles,
FileText,
Library,
LayoutTemplate,
History,
ArrowRight,
GraduationCap,
} from 'lucide-react'
export const Route = createFileRoute( export const Route = createFileRoute('/planes/$planId/asignaturas/')({
'/planes/$planId/asignaturas/' beforeLoad: ({ params }) => {
)({ throw redirect({
component: MateriasLandingPage, to: '/planes/$planId/materias',
params,
})
},
}) })
export default function MateriasLandingPage() {
return (
<div className="w-full">
{/* ================= HERO ================= */}
<section className="bg-gradient-to-b from-[#0b1d3a] to-[#0e2a5c] text-white">
<div className="max-w-7xl mx-auto px-6 py-28">
<div className="flex items-center gap-2 mb-6 text-sm text-blue-200">
<GraduationCap className="w-5 h-5 text-yellow-400" />
<span>SISTEMA DE GESTIÓN CURRICULAR</span>
</div>
<h1 className="text-5xl font-bold mb-6">
Universidad La Salle
</h1>
<p className="max-w-xl text-lg text-blue-100 mb-10">
Diseña, documenta y mejora programas de estudio con herramientas
de inteligencia artificial integradas y cumplimiento normativo SEP.
</p>
<Button
size="lg"
className="bg-yellow-400 text-black hover:bg-yellow-300 font-semibold"
>
Ver materia de ejemplo
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</section>
{/* ================= FEATURES ================= */}
<section className="bg-white py-24">
<div className="max-w-7xl mx-auto px-6">
<h2 className="text-center text-2xl font-semibold mb-14">
Características principales
</h2>
<div className="grid gap-8 md:grid-cols-3">
<FeatureCard
icon={<BookOpen />}
title="Gestión de Materias"
description="Edita datos generales, contenido temático y bibliografía con una interfaz intuitiva."
/>
<FeatureCard
icon={<Sparkles />}
title="IA Integrada"
description="Usa inteligencia artificial para mejorar objetivos, competencias y alinear con perfiles de egreso."
/>
<FeatureCard
icon={<FileText />}
title="Documentos SEP"
description="Genera automáticamente documentos oficiales para la Secretaría de Educación Pública."
/>
<FeatureCard
icon={<Library />}
title="Biblioteca Digital"
description="Busca y vincula recursos del repositorio de Biblioteca La Salle directamente."
/>
<FeatureCard
icon={<LayoutTemplate />}
title="Plantillas Flexibles"
description="Adapta la estructura de materias según plantillas SEP o institucionales."
/>
<FeatureCard
icon={<History />}
title="Historial Completo"
description="Rastrea todos los cambios con historial detallado por usuario y fecha."
/>
</div>
</div>
</section>
{/* ================= CTA ================= */}
<section className="bg-gray-50 py-20">
<div className="max-w-3xl mx-auto text-center px-6">
<h3 className="text-xl font-semibold mb-4">
Explora la vista de detalle de materia
</h3>
<p className="text-muted-foreground mb-8">
Navega por las diferentes pestañas para ver cómo funciona el sistema
de gestión curricular.
</p>
<Button size="lg" className="bg-[#0e2a5c] hover:bg-[#0b1d3a]">
Ir a Inteligencia Artificial Aplicada
<ArrowRight className="ml-2 h-4 w-4" />
</Button>
</div>
</section>
</div>
)
}
/* ================= FEATURE CARD ================= */
function FeatureCard({
icon,
title,
description,
}: {
icon: React.ReactNode
title: string
description: string
}) {
return (
<Card className="border border-gray-200 shadow-sm">
<CardContent className="p-6 space-y-4">
<div className="w-10 h-10 rounded-md bg-yellow-100 text-yellow-600 flex items-center justify-center">
{icon}
</div>
<h4 className="font-semibold">{title}</h4>
<p className="text-sm text-muted-foreground">
{description}
</p>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,10 @@
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/planes/$planId/')({
beforeLoad: ({ params }) => {
throw redirect({
to: '/planes/$planId/datos',
params,
})
},
})

View File

@@ -223,7 +223,17 @@ function RouteComponent() {
estado={estado?.etiqueta ?? 'Desconocido'} estado={estado?.etiqueta ?? 'Desconocido'}
claseColorEstado={estadoColor} claseColorEstado={estadoColor}
colorFacultad={facultad?.color ?? '#000000'} colorFacultad={facultad?.color ?? '#000000'}
onClick={() => console.log('Ver plan', plan.id)} onClick={() =>
navigate({
to: '/planes/$planId',
params: {
planId: plan.id,
},
state: {
realId: plan.id, // 👈 ID largo oculto
} as any,
})
}
/> />
) )
})} })}

View File

@@ -1,102 +1,107 @@
export type PlanStatus = export type PlanStatus =
| 'borrador' | 'borrador'
| 'revision' | 'revision'
| 'expertos' | 'expertos'
| 'consejo' | 'consejo'
| 'aprobado' | 'aprobado'
| 'rechazado'; | 'rechazado'
export type TipoPlan = 'Licenciatura' | 'Maestría' | 'Doctorado' | 'Especialidad'; export type TipoPlan =
| 'Licenciatura'
| 'Maestría'
| 'Doctorado'
| 'Especialidad'
export type TipoMateria = 'obligatoria' | 'optativa' | 'troncal'; export type TipoMateria = 'obligatoria' | 'optativa' | 'troncal'
export type MateriaStatus = 'borrador' | 'revisada' | 'aprobada'; export type MateriaStatus = 'borrador' | 'revisada' | 'aprobada'
export interface Facultad { export interface Facultad {
id: string; id: string
nombre: string; nombre: string
color: string; color: string
icono: string; icono: string
} }
export interface Carrera { export interface Carrera {
id: string; id: string
nombre: string; nombre: string
facultadId: string; facultadId: string
} }
export interface LineaCurricular { export interface LineaCurricular {
id: string; id: string
nombre: string; nombre: string
orden: number; orden: number
color?: string; color?: string
} }
export interface Materia { export interface Materia {
id: string; id: string
clave: string; clave: string
nombre: string; nombre: string
creditos: number; creditos: number
ciclo: number | null; ciclo: number | null
lineaCurricularId: string | null; lineaCurricularId: string | null
tipo: TipoMateria; tipo: TipoMateria
estado: MateriaStatus; estado: MateriaStatus
orden?: number; orden?: number
hd: number; // <--- Añadir hd: number // <--- Añadir
hi: number; // <--- Añadir hi: number // <--- Añadir
prerrequisitos: Array<string>
} }
export interface Plan { export interface Plan {
id: string; id: string
nombre: string; nombre: string
carrera: Carrera; carrera: Carrera
facultad: Facultad; facultad: Facultad
tipoPlan: TipoPlan; tipoPlan: TipoPlan
nivel?: string; nivel?: string
modalidad?: string; modalidad?: string
duracionCiclos: number; duracionCiclos: number
creditosTotales: number; creditosTotales: number
fechaCreacion: string; fechaCreacion: string
estadoActual: PlanStatus; estadoActual: PlanStatus
} }
export interface DatosGeneralesField { export interface DatosGeneralesField {
id: string; id: string
label: string; label: string
value: string; value: string
tipo: 'texto' | 'lista' | 'parrafo'; tipo: 'texto' | 'lista' | 'parrafo'
requerido: boolean; requerido: boolean
} }
export interface CambioPlan { export interface CambioPlan {
id: string; id: string
fecha: string; fecha: string
usuario: string; usuario: string
tab: string; tab: string
descripcion: string; descripcion: string
detalle?: string; detalle?: string
} }
export interface ComentarioFlujo { export interface ComentarioFlujo {
id: string; id: string
usuario: string; usuario: string
fecha: string; fecha: string
texto: string; texto: string
fase: PlanStatus; fase: PlanStatus
} }
export interface DocumentoPlan { export interface DocumentoPlan {
id: string; id: string
fechaGeneracion: string; fechaGeneracion: string
version: number; version: number
url?: string; url?: string
} }
export type PlanTab = export type PlanTab =
| 'datos-generales' | 'datos-generales'
| 'mapa-curricular' | 'mapa-curricular'
| 'materias' | 'materias'
| 'flujo' | 'flujo'
| 'ia' | 'ia'
| 'documento' | 'documento'
| 'historial'; | 'historial'