Compare commits
16 Commits
c9ab32598a
...
04b8c45987
| Author | SHA1 | Date | |
|---|---|---|---|
| 04b8c45987 | |||
| a65e34b41c | |||
| d0e095c979 | |||
| 684a3d8662 | |||
| 6a2a4c0f05 | |||
| f535eea085 | |||
| 09e9e03767 | |||
| 8d20fd4492 | |||
| cc3d2497b7 | |||
| 8dc45d526f | |||
| 0069775ed4 | |||
| 50aec499c7 | |||
| 2b107af73c | |||
| b4570f56b4 | |||
| d0b80b77f5 | |||
| 2f4f445ff0 |
22
.vscode/launch.json
vendored
Normal file
22
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "chrome",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Chrome against localhost",
|
||||||
|
"url": "http://localhost:3000",
|
||||||
|
"webRoot": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "msedge",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Launch Edge against localhost",
|
||||||
|
"url": "http://localhost:3000",
|
||||||
|
"webRoot": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -13,6 +13,7 @@
|
|||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.eslint": "explicit"
|
"source.fixAll.eslint": "explicit"
|
||||||
},
|
},
|
||||||
|
"editor.tabSize": 2,
|
||||||
"eslint.validate": [
|
"eslint.validate": [
|
||||||
"javascript",
|
"javascript",
|
||||||
"javascriptreact",
|
"javascriptreact",
|
||||||
|
|||||||
143
bun.lock
143
bun.lock
@@ -6,8 +6,14 @@
|
|||||||
"name": "acad-ia-2",
|
"name": "acad-ia-2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-avatar": "^1.1.11",
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@stepperize/react": "^5.1.9",
|
||||||
"@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",
|
||||||
@@ -17,6 +23,7 @@
|
|||||||
"@tanstack/router-plugin": "^1.132.0",
|
"@tanstack/router-plugin": "^1.132.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"lucide-react": "^0.561.0",
|
"lucide-react": "^0.561.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
@@ -207,6 +214,14 @@
|
|||||||
|
|
||||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
||||||
|
|
||||||
|
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
|
||||||
|
|
||||||
|
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
|
||||||
|
|
||||||
|
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="],
|
||||||
|
|
||||||
|
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
|
||||||
|
|
||||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||||
|
|
||||||
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
|
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
|
||||||
@@ -231,24 +246,74 @@
|
|||||||
|
|
||||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
|
||||||
|
|
||||||
|
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||||
|
|
||||||
|
"@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-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.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-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-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
|
||||||
|
|
||||||
|
"@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-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-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
|
||||||
|
|
||||||
|
"@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-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-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@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-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
|
||||||
|
|
||||||
|
"@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.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-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-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=="],
|
||||||
|
|
||||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "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-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "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-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "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-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", "@radix-ui/react-visually-hidden": "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-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
|
||||||
|
|
||||||
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
|
||||||
|
|
||||||
"@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="],
|
"@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="],
|
||||||
|
|
||||||
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
|
||||||
|
|
||||||
|
"@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "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-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="],
|
||||||
|
|
||||||
|
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
|
||||||
|
|
||||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="],
|
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="],
|
||||||
|
|
||||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.5", "", { "os": "android", "cpu": "arm" }, "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ=="],
|
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.5", "", { "os": "android", "cpu": "arm" }, "sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ=="],
|
||||||
@@ -309,6 +374,10 @@
|
|||||||
|
|
||||||
"@solid-primitives/utils": ["@solid-primitives/utils@6.3.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ=="],
|
"@solid-primitives/utils": ["@solid-primitives/utils@6.3.2", "", { "peerDependencies": { "solid-js": "^1.6.12" } }, "sha512-hZ/M/qr25QOCcwDPOHtGjxTD8w2mNyVAYvcfgwzBHq2RwNqHNdDNsMZYap20+ruRwW4A3Cdkczyoz0TSxLCAPQ=="],
|
||||||
|
|
||||||
|
"@stepperize/core": ["@stepperize/core@1.2.7", "", { "peerDependencies": { "typescript": ">=5.0.2" } }, "sha512-XiUwLZ0XRAfaDK6AzWVgqvI/BcrylyplhUXKO8vzgRw0FTmyMKHAAbQLDvU//ZJAqnmG2cSLZDSkcwLxU5zSYA=="],
|
||||||
|
|
||||||
|
"@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.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=="],
|
"@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=="],
|
||||||
|
|
||||||
"@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=="],
|
||||||
@@ -511,6 +580,8 @@
|
|||||||
|
|
||||||
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
|
|
||||||
|
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
|
||||||
|
|
||||||
"aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
|
"aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
|
||||||
|
|
||||||
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
|
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
|
||||||
@@ -579,6 +650,8 @@
|
|||||||
|
|
||||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
@@ -625,6 +698,8 @@
|
|||||||
|
|
||||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||||
|
|
||||||
|
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
|
||||||
|
|
||||||
"diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="],
|
"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=="],
|
||||||
@@ -743,6 +818,8 @@
|
|||||||
|
|
||||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||||
|
|
||||||
|
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
|
||||||
|
|
||||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||||
|
|
||||||
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
||||||
@@ -1003,6 +1080,12 @@
|
|||||||
|
|
||||||
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
|
"react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="],
|
||||||
|
|
||||||
|
"react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
|
||||||
|
|
||||||
|
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
|
||||||
"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=="],
|
||||||
@@ -1165,6 +1248,10 @@
|
|||||||
|
|
||||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||||
|
|
||||||
|
"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-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.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": ["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=="],
|
||||||
@@ -1221,6 +1308,50 @@
|
|||||||
|
|
||||||
"@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-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-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-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-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-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-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-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-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-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.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="],
|
||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
|
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="],
|
||||||
@@ -1281,6 +1412,18 @@
|
|||||||
|
|
||||||
"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=="],
|
||||||
|
|
||||||
|
"@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=="],
|
||||||
|
|
||||||
|
"@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-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=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||||
|
|||||||
@@ -18,8 +18,14 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@radix-ui/react-avatar": "^1.1.11",
|
"@radix-ui/react-avatar": "^1.1.11",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-label": "^2.1.8",
|
||||||
|
"@radix-ui/react-popover": "^1.1.15",
|
||||||
|
"@radix-ui/react-select": "^2.2.6",
|
||||||
"@radix-ui/react-separator": "^1.1.8",
|
"@radix-ui/react-separator": "^1.1.8",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
|
"@stepperize/react": "^5.1.9",
|
||||||
"@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",
|
||||||
@@ -29,6 +35,7 @@
|
|||||||
"@tanstack/router-plugin": "^1.132.0",
|
"@tanstack/router-plugin": "^1.132.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"lucide-react": "^0.561.0",
|
"lucide-react": "^0.561.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const config = {
|
|||||||
semi: false,
|
semi: false,
|
||||||
singleQuote: true,
|
singleQuote: true,
|
||||||
trailingComma: 'all',
|
trailingComma: 'all',
|
||||||
|
tabWidth: 2,
|
||||||
plugins: ['prettier-plugin-tailwindcss'],
|
plugins: ['prettier-plugin-tailwindcss'],
|
||||||
tailwindFunctions: ['clsx', 'cn', 'cva'],
|
tailwindFunctions: ['clsx', 'cn', 'cva'],
|
||||||
endOfLine: 'lf',
|
endOfLine: 'lf',
|
||||||
|
|||||||
74
src/components/CircularProgress.tsx
Normal file
74
src/components/CircularProgress.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
interface CircularProgressProps {
|
||||||
|
current: number
|
||||||
|
total: number
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CircularProgress({
|
||||||
|
current,
|
||||||
|
total,
|
||||||
|
className,
|
||||||
|
}: CircularProgressProps) {
|
||||||
|
// Configuración interna del SVG (Coordenadas 100x100)
|
||||||
|
const center = 50
|
||||||
|
const strokeWidth = 8 // Grosor de la línea
|
||||||
|
const radius = 40 // Radio (dejamos margen para el borde)
|
||||||
|
const circumference = 2 * Math.PI * radius
|
||||||
|
|
||||||
|
// Cálculo del porcentaje inverso (para que se llene correctamente)
|
||||||
|
const percentage = (current / total) * 100
|
||||||
|
const strokeDashoffset = circumference - (percentage / 100) * circumference
|
||||||
|
|
||||||
|
return (
|
||||||
|
// CAMBIO CLAVE 1: 'size-24' (96px) da mucho más aire que 'size-16'
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative flex size-20 items-center justify-center',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/* CAMBIO CLAVE 2: Contenedor de texto con inset-0 para centrado perfecto */}
|
||||||
|
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||||
|
<span className="mb-1 text-sm leading-none font-medium text-slate-500">
|
||||||
|
Paso
|
||||||
|
</span>
|
||||||
|
<span className="text-base leading-none font-bold text-slate-900">
|
||||||
|
{current}{' '}
|
||||||
|
<span className="text-base font-normal text-slate-400">
|
||||||
|
/ {total}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SVG con viewBox para escalar automáticamente */}
|
||||||
|
<svg className="size-full -rotate-90" viewBox="0 0 100 100">
|
||||||
|
{/* Círculo de Fondo (Gris claro) */}
|
||||||
|
<circle
|
||||||
|
cx={center}
|
||||||
|
cy={center}
|
||||||
|
r={radius}
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fill="transparent"
|
||||||
|
className="text-slate-100"
|
||||||
|
/>
|
||||||
|
{/* Círculo de Progreso (Verde/Color principal) */}
|
||||||
|
<circle
|
||||||
|
cx={center}
|
||||||
|
cy={center}
|
||||||
|
r={radius}
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
fill="transparent"
|
||||||
|
strokeDasharray={circumference}
|
||||||
|
strokeDashoffset={strokeDashoffset}
|
||||||
|
strokeLinecap="round"
|
||||||
|
className="text-primary transition-all duration-500 ease-out"
|
||||||
|
// Nota: usa text-primary para tomar el color de tu tema, o pon text-green-500
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
155
src/components/asignaturas/wizard/PasoBasicosForm.tsx
Normal file
155
src/components/asignaturas/wizard/PasoBasicosForm.tsx
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import type {
|
||||||
|
NewSubjectWizardState,
|
||||||
|
TipoAsignatura,
|
||||||
|
} from '@/features/asignaturas/new/types'
|
||||||
|
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
import {
|
||||||
|
ESTRUCTURAS_SEP,
|
||||||
|
TIPOS_MATERIA,
|
||||||
|
} from '@/features/asignaturas/new/catalogs'
|
||||||
|
|
||||||
|
export function PasoBasicosForm({
|
||||||
|
wizard,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
wizard: NewSubjectWizardState
|
||||||
|
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="grid gap-1 sm:col-span-2">
|
||||||
|
<Label htmlFor="nombre">Nombre de la asignatura</Label>
|
||||||
|
<Input
|
||||||
|
id="nombre"
|
||||||
|
placeholder="Ej. Matemáticas Discretas"
|
||||||
|
value={wizard.datosBasicos.nombre}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, nombre: e.target.value },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="clave">Clave (Opcional)</Label>
|
||||||
|
<Input
|
||||||
|
id="clave"
|
||||||
|
placeholder="Ej. MAT-101"
|
||||||
|
value={wizard.datosBasicos.clave || ''}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, clave: e.target.value },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="tipo">Tipo</Label>
|
||||||
|
<Select
|
||||||
|
value={wizard.datosBasicos.tipo}
|
||||||
|
onValueChange={(val) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, tipo: val as TipoAsignatura },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="tipo"
|
||||||
|
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
||||||
|
>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{TIPOS_MATERIA.map((t) => (
|
||||||
|
<SelectItem key={t.value} value={t.value}>
|
||||||
|
{t.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="creditos">Créditos</Label>
|
||||||
|
<Input
|
||||||
|
id="creditos"
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={wizard.datosBasicos.creditos}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
creditos: Number(e.target.value || 0),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="horas">Horas / Semana</Label>
|
||||||
|
<Input
|
||||||
|
id="horas"
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={wizard.datosBasicos.horasSemana || 0}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
horasSemana: Number(e.target.value || 0),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1 sm:col-span-2">
|
||||||
|
<Label htmlFor="estructura">Estructura de la asignatura</Label>
|
||||||
|
<Select
|
||||||
|
value={wizard.datosBasicos.estructuraId}
|
||||||
|
onValueChange={(val) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, estructuraId: val },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="estructura"
|
||||||
|
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Selecciona plantilla..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{ESTRUCTURAS_SEP.map((e) => (
|
||||||
|
<SelectItem key={e.id} value={e.id}>
|
||||||
|
{e.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
Define los campos requeridos (ej. Objetivos, Temario, Evaluación).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
286
src/components/asignaturas/wizard/PasoConfiguracionPanel.tsx
Normal file
286
src/components/asignaturas/wizard/PasoConfiguracionPanel.tsx
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
import * as Icons from 'lucide-react'
|
||||||
|
|
||||||
|
import type { NewSubjectWizardState } from '@/features/asignaturas/new/types'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
|
import {
|
||||||
|
ARCHIVOS_SISTEMA_MOCK,
|
||||||
|
FACULTADES,
|
||||||
|
MATERIAS_MOCK,
|
||||||
|
PLANES_MOCK,
|
||||||
|
} from '@/features/asignaturas/new/catalogs'
|
||||||
|
|
||||||
|
export function PasoConfiguracionPanel({
|
||||||
|
wizard,
|
||||||
|
onChange,
|
||||||
|
onGenerarIA,
|
||||||
|
}: {
|
||||||
|
wizard: NewSubjectWizardState
|
||||||
|
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
||||||
|
onGenerarIA: () => void
|
||||||
|
}) {
|
||||||
|
if (wizard.modoCreacion === 'MANUAL') {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Configuración Manual</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
La asignatura se creará vacía. Podrás editar el contenido detallado
|
||||||
|
en la siguiente pantalla.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wizard.modoCreacion === 'IA') {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label>Descripción del enfoque</Label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Ej. Asignatura teórica-práctica enfocada en patrones de diseño..."
|
||||||
|
value={wizard.iaConfig?.descripcionEnfoque}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
iaConfig: {
|
||||||
|
...w.iaConfig!,
|
||||||
|
descripcionEnfoque: e.target.value,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className="min-h-[100px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label>Notas adicionales</Label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Restricciones, bibliografía sugerida, etc."
|
||||||
|
value={wizard.iaConfig?.notasAdicionales}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
iaConfig: { ...w.iaConfig!, notasAdicionales: e.target.value },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<Label>Archivos de contexto (Opcional)</Label>
|
||||||
|
<div className="flex flex-col gap-2 rounded-md border p-3">
|
||||||
|
{ARCHIVOS_SISTEMA_MOCK.map((file) => (
|
||||||
|
<div key={file.id} className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id={file.id}
|
||||||
|
checked={wizard.iaConfig?.archivosExistentesIds.includes(
|
||||||
|
file.id,
|
||||||
|
)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const checked = e.target.checked
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
iaConfig: {
|
||||||
|
...w.iaConfig!,
|
||||||
|
archivosExistentesIds: checked
|
||||||
|
? [
|
||||||
|
...(w.iaConfig?.archivosExistentesIds || []),
|
||||||
|
file.id,
|
||||||
|
]
|
||||||
|
: w.iaConfig?.archivosExistentesIds.filter(
|
||||||
|
(id) => id !== file.id,
|
||||||
|
) || [],
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Label htmlFor={file.id} className="font-normal">
|
||||||
|
{file.name}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button onClick={onGenerarIA} disabled={wizard.isLoading}>
|
||||||
|
{wizard.isLoading ? (
|
||||||
|
<>
|
||||||
|
<Icons.Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
Generando...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icons.Sparkles className="mr-2 h-4 w-4" /> Generar Preview
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{wizard.resumen.previewAsignatura && (
|
||||||
|
<Card className="bg-muted/50 border-dashed">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-base">Vista previa generada</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="text-muted-foreground text-sm">
|
||||||
|
<p>
|
||||||
|
<strong>Objetivo:</strong>{' '}
|
||||||
|
{wizard.resumen.previewAsignatura.objetivo}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2">
|
||||||
|
Se detectaron {wizard.resumen.previewAsignatura.unidades}{' '}
|
||||||
|
unidades temáticas y{' '}
|
||||||
|
{wizard.resumen.previewAsignatura.bibliografiaCount} fuentes
|
||||||
|
bibliográficas.
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wizard.subModoClonado === 'INTERNO') {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid gap-2 sm:grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<Label>Facultad</Label>
|
||||||
|
<Select
|
||||||
|
onValueChange={(val) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
clonInterno: { ...w.clonInterno, facultadId: val },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Todas" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{FACULTADES.map((f) => (
|
||||||
|
<SelectItem key={f.id} value={f.id}>
|
||||||
|
{f.nombre}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Plan</Label>
|
||||||
|
<Select
|
||||||
|
onValueChange={(val) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
clonInterno: { ...w.clonInterno, planOrigenId: val },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Todos" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{PLANES_MOCK.map((p) => (
|
||||||
|
<SelectItem key={p.id} value={p.id}>
|
||||||
|
{p.nombre}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Buscar</Label>
|
||||||
|
<Input placeholder="Nombre..." />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid max-h-[300px] gap-2 overflow-y-auto">
|
||||||
|
{MATERIAS_MOCK.map((m) => (
|
||||||
|
<div
|
||||||
|
key={m.id}
|
||||||
|
onClick={() =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
clonInterno: { ...w.clonInterno, asignaturaOrigenId: m.id },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className={`hover:bg-accent flex cursor-pointer items-center justify-between rounded-md border p-3 ${
|
||||||
|
wizard.clonInterno?.asignaturaOrigenId === m.id
|
||||||
|
? 'border-primary bg-primary/5 ring-primary ring-1'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{m.nombre}</div>
|
||||||
|
<div className="text-muted-foreground text-xs">
|
||||||
|
{m.clave} • {m.creditos} créditos
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{wizard.clonInterno?.asignaturaOrigenId === m.id && (
|
||||||
|
<Icons.CheckCircle2 className="text-primary h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wizard.subModoClonado === 'TRADICIONAL') {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="rounded-lg border border-dashed p-8 text-center">
|
||||||
|
<Icons.Upload className="text-muted-foreground mx-auto mb-4 h-10 w-10" />
|
||||||
|
<h3 className="mb-1 text-sm font-medium">
|
||||||
|
Sube el Word de la asignatura
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground mb-4 text-xs">
|
||||||
|
Arrastra el archivo o haz clic para buscar (.doc, .docx)
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
accept=".doc,.docx"
|
||||||
|
className="mx-auto max-w-xs"
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
clonTradicional: {
|
||||||
|
...w.clonTradicional!,
|
||||||
|
archivoWordAsignaturaId:
|
||||||
|
e.target.files?.[0]?.name || 'mock_file',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{wizard.clonTradicional?.archivoWordAsignaturaId && (
|
||||||
|
<div className="flex items-center gap-2 rounded-md bg-green-50 p-3 text-sm text-green-700">
|
||||||
|
<Icons.FileText className="h-4 w-4" />
|
||||||
|
Archivo cargado listo para procesar.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
135
src/components/asignaturas/wizard/PasoMetodoCardGroup.tsx
Normal file
135
src/components/asignaturas/wizard/PasoMetodoCardGroup.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import * as Icons from 'lucide-react'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ModoCreacion,
|
||||||
|
NewSubjectWizardState,
|
||||||
|
SubModoClonado,
|
||||||
|
} from '@/features/asignaturas/new/types'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
|
||||||
|
export function PasoMetodoCardGroup({
|
||||||
|
wizard,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
wizard: NewSubjectWizardState
|
||||||
|
onChange: React.Dispatch<React.SetStateAction<NewSubjectWizardState>>
|
||||||
|
}) {
|
||||||
|
const isSelected = (m: ModoCreacion) => wizard.modoCreacion === m
|
||||||
|
const isSubSelected = (s: SubModoClonado) => wizard.subModoClonado === s
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4 sm:grid-cols-3">
|
||||||
|
<Card
|
||||||
|
className={isSelected('MANUAL') ? 'ring-ring ring-2' : ''}
|
||||||
|
onClick={() =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
modoCreacion: 'MANUAL',
|
||||||
|
subModoClonado: undefined,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Icons.Pencil className="text-primary h-5 w-5" /> Manual
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Asignatura vacía con estructura base.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className={isSelected('IA') ? 'ring-ring ring-2' : ''}
|
||||||
|
onClick={() =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
modoCreacion: 'IA',
|
||||||
|
subModoClonado: undefined,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Icons.Sparkles className="text-primary h-5 w-5" /> Con IA
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Generar contenido automático.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className={isSelected('CLONADO') ? 'ring-ring ring-2' : ''}
|
||||||
|
onClick={() => onChange((w) => ({ ...w, modoCreacion: 'CLONADO' }))}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Icons.Copy className="text-primary h-5 w-5" /> Clonado
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>De otra asignatura o archivo Word.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
{wizard.modoCreacion === 'CLONADO' && (
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onChange((w) => ({ ...w, subModoClonado: 'INTERNO' }))
|
||||||
|
}}
|
||||||
|
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer items-center gap-4 rounded-lg border p-4 text-left transition-all ${
|
||||||
|
isSubSelected('INTERNO')
|
||||||
|
? 'bg-primary/5 text-primary ring-primary border-primary ring-1'
|
||||||
|
: 'border-border text-muted-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icons.Database className="h-6 w-6 flex-none" />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium">Del sistema</span>
|
||||||
|
<span className="text-xs opacity-70">
|
||||||
|
Buscar en otros planes
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onChange((w) => ({ ...w, subModoClonado: 'TRADICIONAL' }))
|
||||||
|
}}
|
||||||
|
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer items-center gap-4 rounded-lg border p-4 text-left transition-all ${
|
||||||
|
isSubSelected('TRADICIONAL')
|
||||||
|
? 'bg-primary/5 text-primary ring-primary border-primary ring-1'
|
||||||
|
: 'border-border text-muted-foreground'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icons.Upload className="h-6 w-6 flex-none" />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-sm font-medium">Desde archivos</span>
|
||||||
|
<span className="text-xs opacity-70">
|
||||||
|
Subir Word existente
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
75
src/components/asignaturas/wizard/PasoResumenCard.tsx
Normal file
75
src/components/asignaturas/wizard/PasoResumenCard.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import * as Icons from 'lucide-react'
|
||||||
|
|
||||||
|
import type { NewSubjectWizardState } from '@/features/asignaturas/new/types'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
import { ESTRUCTURAS_SEP } from '@/features/asignaturas/new/catalogs'
|
||||||
|
|
||||||
|
export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Resumen de creación</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Verifica los datos antes de crear la asignatura.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid gap-4 text-sm">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Nombre:</span>
|
||||||
|
<div className="font-medium">{wizard.datosBasicos.nombre}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Tipo:</span>
|
||||||
|
<div className="font-medium">{wizard.datosBasicos.tipo}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Créditos:</span>
|
||||||
|
<div className="font-medium">{wizard.datosBasicos.creditos}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Estructura:</span>
|
||||||
|
<div className="font-medium">
|
||||||
|
{
|
||||||
|
ESTRUCTURAS_SEP.find(
|
||||||
|
(e) => e.id === wizard.datosBasicos.estructuraId,
|
||||||
|
)?.label
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-muted rounded-md p-3">
|
||||||
|
<span className="text-muted-foreground">Modo de creación:</span>
|
||||||
|
<div className="flex items-center gap-2 font-medium">
|
||||||
|
{wizard.modoCreacion === 'MANUAL' && (
|
||||||
|
<>
|
||||||
|
<Icons.Pencil className="h-4 w-4" /> Manual (Vacía)
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{wizard.modoCreacion === 'IA' && (
|
||||||
|
<>
|
||||||
|
<Icons.Sparkles className="h-4 w-4" /> Generada con IA
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{wizard.modoCreacion === 'CLONADO' && (
|
||||||
|
<>
|
||||||
|
<Icons.Copy className="h-4 w-4" /> Clonada
|
||||||
|
{wizard.subModoClonado === 'INTERNO'
|
||||||
|
? ' (Sistema)'
|
||||||
|
: ' (Archivo)'}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
40
src/components/asignaturas/wizard/StepWithTooltip.tsx
Normal file
40
src/components/asignaturas/wizard/StepWithTooltip.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip'
|
||||||
|
|
||||||
|
export function StepWithTooltip({
|
||||||
|
title,
|
||||||
|
desc,
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
desc: string
|
||||||
|
}) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
return (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span
|
||||||
|
className="cursor-help decoration-dotted underline-offset-4 hover:underline"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setIsOpen((prev) => !prev)
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setIsOpen(true)}
|
||||||
|
onMouseLeave={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-[200px] text-xs">
|
||||||
|
<p>{desc}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
39
src/components/asignaturas/wizard/VistaSinPermisos.tsx
Normal file
39
src/components/asignaturas/wizard/VistaSinPermisos.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import * as Icons from 'lucide-react'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
import { DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
|
|
||||||
|
export function VistaSinPermisos({ onClose }: { onClose: () => void }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DialogHeader className="flex-none border-b p-6">
|
||||||
|
<DialogTitle>Nueva Asignatura</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex-1 p-6">
|
||||||
|
<Card className="border-destructive/40">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-destructive flex items-center gap-2">
|
||||||
|
<Icons.ShieldAlert className="h-5 w-5" />
|
||||||
|
Sin permisos
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Solo el Jefe de Carrera puede crear asignaturas.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex justify-end">
|
||||||
|
<Button variant="secondary" onClick={onClose}>
|
||||||
|
Volver
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
66
src/components/asignaturas/wizard/WizardControls.tsx
Normal file
66
src/components/asignaturas/wizard/WizardControls.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import type { NewSubjectWizardState } from '@/features/asignaturas/new/types'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
export function WizardControls({
|
||||||
|
Wizard,
|
||||||
|
methods,
|
||||||
|
wizard,
|
||||||
|
canContinueDesdeMetodo,
|
||||||
|
canContinueDesdeBasicos,
|
||||||
|
canContinueDesdeConfig,
|
||||||
|
onCreate,
|
||||||
|
}: {
|
||||||
|
Wizard: any
|
||||||
|
methods: any
|
||||||
|
wizard: NewSubjectWizardState
|
||||||
|
canContinueDesdeMetodo: boolean
|
||||||
|
canContinueDesdeBasicos: boolean
|
||||||
|
canContinueDesdeConfig: boolean
|
||||||
|
onCreate: () => void
|
||||||
|
}) {
|
||||||
|
const idx = Wizard.utils.getIndex(methods.current.id)
|
||||||
|
const isLast = idx >= Wizard.steps.length - 1
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex-none border-t bg-white p-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
{wizard.errorMessage && (
|
||||||
|
<span className="text-destructive text-sm font-medium">
|
||||||
|
{wizard.errorMessage}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => methods.prev()}
|
||||||
|
disabled={idx === 0 || wizard.isLoading}
|
||||||
|
>
|
||||||
|
Anterior
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{!isLast ? (
|
||||||
|
<Button
|
||||||
|
onClick={() => methods.next()}
|
||||||
|
disabled={
|
||||||
|
wizard.isLoading ||
|
||||||
|
(idx === 0 && !canContinueDesdeMetodo) ||
|
||||||
|
(idx === 1 && !canContinueDesdeBasicos) ||
|
||||||
|
(idx === 2 && !canContinueDesdeConfig)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Siguiente
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button onClick={onCreate} disabled={wizard.isLoading}>
|
||||||
|
{wizard.isLoading ? 'Creando...' : 'Crear Asignatura'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
80
src/components/asignaturas/wizard/WizardHeader.tsx
Normal file
80
src/components/asignaturas/wizard/WizardHeader.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import * as Icons from 'lucide-react'
|
||||||
|
|
||||||
|
import { StepWithTooltip } from './StepWithTooltip'
|
||||||
|
|
||||||
|
import { CircularProgress } from '@/components/CircularProgress'
|
||||||
|
import { DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
|
|
||||||
|
export function WizardHeader({
|
||||||
|
title,
|
||||||
|
Wizard,
|
||||||
|
methods,
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
Wizard: any
|
||||||
|
methods: any
|
||||||
|
}) {
|
||||||
|
const currentIndex = Wizard.utils.getIndex(methods.current.id) + 1
|
||||||
|
const totalSteps = Wizard.steps.length
|
||||||
|
const nextStep = Wizard.steps[currentIndex]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="z-10 flex-none border-b bg-white">
|
||||||
|
<div className="flex items-center justify-between p-6 pb-4">
|
||||||
|
<DialogHeader className="p-0">
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
{methods.onClose && (
|
||||||
|
<button
|
||||||
|
onClick={methods.onClose}
|
||||||
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none"
|
||||||
|
>
|
||||||
|
<Icons.X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Cerrar</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-6 pb-6">
|
||||||
|
<div className="block sm:hidden">
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<CircularProgress current={currentIndex} total={totalSteps} />
|
||||||
|
<div className="flex flex-col justify-center">
|
||||||
|
<h2 className="text-lg font-bold text-slate-900">
|
||||||
|
<StepWithTooltip
|
||||||
|
title={methods.current.title}
|
||||||
|
desc={methods.current.description}
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
{nextStep ? (
|
||||||
|
<p className="text-sm text-slate-400">
|
||||||
|
Siguiente: {nextStep.title}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm font-medium text-green-500">
|
||||||
|
¡Último paso!
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<Wizard.Stepper.Navigation className="border-border/60 rounded-xl border bg-slate-50 p-2">
|
||||||
|
{Wizard.steps.map((step: any) => (
|
||||||
|
<Wizard.Stepper.Step
|
||||||
|
key={step.id}
|
||||||
|
of={step.id}
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<Wizard.Stepper.Title>
|
||||||
|
<StepWithTooltip title={step.title} desc={step.description} />
|
||||||
|
</Wizard.Stepper.Title>
|
||||||
|
</Wizard.Stepper.Step>
|
||||||
|
))}
|
||||||
|
</Wizard.Stepper.Navigation>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
//import { supabase } from '@/lib/supabase'
|
|
||||||
import { Input } from '../ui/Input'
|
// import { supabase } from '@/lib/supabase'
|
||||||
|
import { LoginInput } from '../ui/LoginInput'
|
||||||
import { SubmitButton } from '../ui/SubmitButton'
|
import { SubmitButton } from '../ui/SubmitButton'
|
||||||
|
|
||||||
export function ExternalLoginForm() {
|
export function ExternalLoginForm() {
|
||||||
@@ -8,7 +9,7 @@ export function ExternalLoginForm() {
|
|||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
/*await supabase.auth.signInWithPassword({
|
/* await supabase.auth.signInWithPassword({
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
})*/
|
})*/
|
||||||
@@ -17,13 +18,17 @@ export function ExternalLoginForm() {
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
onSubmit={e => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
submit()
|
submit()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Input label="Correo electrónico" value={email} onChange={setEmail} />
|
<LoginInput
|
||||||
<Input
|
label="Correo electrónico"
|
||||||
|
value={email}
|
||||||
|
onChange={setEmail}
|
||||||
|
/>
|
||||||
|
<LoginInput
|
||||||
label="Contraseña"
|
label="Contraseña"
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
//import { supabase } from '@/lib/supabase'
|
|
||||||
import { Input } from '../ui/Input'
|
// import { supabase } from '@/lib/supabase'
|
||||||
|
import { LoginInput } from '../ui/LoginInput'
|
||||||
import { SubmitButton } from '../ui/SubmitButton'
|
import { SubmitButton } from '../ui/SubmitButton'
|
||||||
|
|
||||||
export function InternalLoginForm() {
|
export function InternalLoginForm() {
|
||||||
@@ -8,7 +9,7 @@ export function InternalLoginForm() {
|
|||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
/*await supabase.auth.signInWithPassword({
|
/* await supabase.auth.signInWithPassword({
|
||||||
email: `${clave}@ulsa.mx`,
|
email: `${clave}@ulsa.mx`,
|
||||||
password,
|
password,
|
||||||
})*/
|
})*/
|
||||||
@@ -17,13 +18,13 @@ export function InternalLoginForm() {
|
|||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
onSubmit={e => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
submit()
|
submit()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Input label="Clave ULSA" value={clave} onChange={setClave} />
|
<LoginInput label="Clave ULSA" value={clave} onChange={setClave} />
|
||||||
<Input
|
<LoginInput
|
||||||
label="Contraseña"
|
label="Contraseña"
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
|
|||||||
@@ -1,27 +1,24 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { LoginTabs } from './LoginTabs.tsx'
|
|
||||||
import { InternalLoginForm } from './InternalLoginForm.tsx'
|
|
||||||
import { ExternalLoginForm } from './ExternalLoginForm.tsx'
|
import { ExternalLoginForm } from './ExternalLoginForm.tsx'
|
||||||
|
import { InternalLoginForm } from './InternalLoginForm.tsx'
|
||||||
|
import { LoginTabs } from './LoginTabs.tsx'
|
||||||
|
|
||||||
export function LoginCard() {
|
export function LoginCard() {
|
||||||
const [type, setType] = useState<'internal' | 'external'>('internal')
|
const [type, setType] = useState<'internal' | 'external'>('internal')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-md bg-white rounded-2xl shadow-xl p-8">
|
<div className="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl">
|
||||||
<h1 className="text-2xl font-semibold text-center mb-1">
|
<h1 className="mb-1 text-center text-2xl font-semibold">
|
||||||
Iniciar sesión
|
Iniciar sesión
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-sm text-gray-500 text-center mb-6">
|
<p className="mb-6 text-center text-sm text-gray-500">
|
||||||
Accede al Sistema de Planes de Estudio
|
Accede al Sistema de Planes de Estudio
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<LoginTabs value={type} onChange={setType} />
|
<LoginTabs value={type} onChange={setType} />
|
||||||
|
|
||||||
{type === 'internal' ? (
|
{type === 'internal' ? <InternalLoginForm /> : <ExternalLoginForm />}
|
||||||
<InternalLoginForm />
|
|
||||||
) : (
|
|
||||||
<ExternalLoginForm />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
39
src/components/planes/BarraBusqueda.tsx
Normal file
39
src/components/planes/BarraBusqueda.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { SearchIcon } from 'lucide-react'
|
||||||
|
import { useId } from 'react'
|
||||||
|
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: string
|
||||||
|
onChange: (value: string) => void
|
||||||
|
placeholder?: string
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const BarraBusqueda: React.FC<Props> = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder = 'Buscar…',
|
||||||
|
className,
|
||||||
|
}) => {
|
||||||
|
const id = useId()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={['relative', className].filter(Boolean).join(' ')}>
|
||||||
|
<div className="text-muted-foreground pointer-events-none absolute inset-y-0 left-0 flex items-center justify-center pl-3 peer-disabled:opacity-50">
|
||||||
|
<SearchIcon className="size-4" />
|
||||||
|
<span className="sr-only">Buscar</span>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
id={id}
|
||||||
|
type="search"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="peer px-9 [&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none [&::-webkit-search-results-button]:appearance-none [&::-webkit-search-results-decoration]:appearance-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BarraBusqueda
|
||||||
108
src/components/planes/Filtro.tsx
Normal file
108
src/components/planes/Filtro.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { CheckIcon, ChevronDown } from 'lucide-react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from '@/components/ui/command'
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@/components/ui/popover'
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
export type Option = { value: string; label: string }
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
options: Array<Option>
|
||||||
|
value: string | null
|
||||||
|
onChange: (value: string) => void
|
||||||
|
placeholder?: string
|
||||||
|
className?: string
|
||||||
|
ariaLabel?: string
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Filtro: React.FC<Props> = ({
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder = 'Seleccionar…',
|
||||||
|
className,
|
||||||
|
ariaLabel,
|
||||||
|
disabled,
|
||||||
|
}) => {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
const label = value
|
||||||
|
? (options.find((o) => o.value === value)?.label ?? placeholder)
|
||||||
|
: placeholder
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={open}
|
||||||
|
className={cn('w-full min-w-0 justify-between', className)}
|
||||||
|
aria-label={ariaLabel ?? 'Filtro combobox'}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<span className="truncate">{label}</span>
|
||||||
|
<ChevronDown className="shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{label}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<PopoverContent className="p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Buscar…" className="h-9" />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>Sin resultados.</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{options.map((opt) => (
|
||||||
|
<CommandItem
|
||||||
|
key={opt.value}
|
||||||
|
value={opt.value}
|
||||||
|
onSelect={(currentValue) => {
|
||||||
|
onChange(currentValue)
|
||||||
|
setOpen(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
'ml-auto',
|
||||||
|
value === opt.value ? 'opacity-100' : 'opacity-0',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Filtro
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ArrowRight } from 'lucide-react'
|
import { ArrowRight } from 'lucide-react'
|
||||||
|
|
||||||
import type {LucideIcon} from 'lucide-react';
|
import type { LucideIcon } from 'lucide-react'
|
||||||
|
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'
|
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'
|
||||||
@@ -36,7 +36,7 @@ export default function PlanEstudiosCard({
|
|||||||
<Card
|
<Card
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
'group relative flex h-full cursor-pointer flex-col justify-between overflow-hidden border-l-4 transition-all hover:shadow-lg',
|
'group relative flex h-full cursor-pointer flex-col justify-between gap-2 overflow-hidden border-l-4 transition-all hover:shadow-lg',
|
||||||
)}
|
)}
|
||||||
// Aplicamos el color de la facultad dinámicamente al borde y un fondo muy sutil
|
// Aplicamos el color de la facultad dinámicamente al borde y un fondo muy sutil
|
||||||
style={{
|
style={{
|
||||||
@@ -61,14 +61,14 @@ export default function PlanEstudiosCard({
|
|||||||
</h4>
|
</h4>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="text-muted-foreground space-y-1 pb-4 text-sm">
|
<CardContent className="text-muted-foreground space-y-1 text-sm">
|
||||||
<p className="text-foreground font-medium">
|
<p className="text-foreground font-medium">
|
||||||
{nivel} • {ciclos}
|
{nivel} • {ciclos}
|
||||||
</p>
|
</p>
|
||||||
<p>{facultad}</p>
|
<p>{facultad}</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<CardFooter className="bg-background/50 flex items-center justify-between border-t px-6 py-3 backdrop-blur-sm">
|
<CardFooter className="bg-background/50 flex items-center justify-between border-t px-6 pb-3 backdrop-blur-sm [.border-t]:pt-3">
|
||||||
<Badge className={`text-sm font-semibold ${claseColorEstado}`}>
|
<Badge className={`text-sm font-semibold ${claseColorEstado}`}>
|
||||||
{estado}
|
{estado}
|
||||||
</Badge>
|
</Badge>
|
||||||
181
src/components/planes/wizard/PasoBasicosForm.tsx
Normal file
181
src/components/planes/wizard/PasoBasicosForm.tsx
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
import type { CARRERAS } from '@/features/planes/new/catalogs'
|
||||||
|
import type { NewPlanWizardState } from '@/features/planes/new/types'
|
||||||
|
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select'
|
||||||
|
import {
|
||||||
|
FACULTADES,
|
||||||
|
NIVELES,
|
||||||
|
TIPOS_CICLO,
|
||||||
|
} from '@/features/planes/new/catalogs'
|
||||||
|
|
||||||
|
export function PasoBasicosForm({
|
||||||
|
wizard,
|
||||||
|
onChange,
|
||||||
|
carrerasFiltradas,
|
||||||
|
}: {
|
||||||
|
wizard: NewPlanWizardState
|
||||||
|
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
|
||||||
|
carrerasFiltradas: typeof CARRERAS
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="grid gap-1 sm:col-span-2">
|
||||||
|
<Label htmlFor="nombrePlan">Nombre del plan</Label>
|
||||||
|
<Input
|
||||||
|
id="nombrePlan"
|
||||||
|
placeholder="Ej. Ingeniería en Sistemas 2026"
|
||||||
|
value={wizard.datosBasicos.nombrePlan}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, nombrePlan: e.target.value },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="facultad">Facultad</Label>
|
||||||
|
<Select
|
||||||
|
value={wizard.datosBasicos.facultadId}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
facultadId: value,
|
||||||
|
carreraId: '',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="facultad"
|
||||||
|
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Selecciona facultad…" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{FACULTADES.map((f) => (
|
||||||
|
<SelectItem key={f.id} value={f.id}>
|
||||||
|
{f.nombre}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="carrera">Carrera</Label>
|
||||||
|
<Select
|
||||||
|
value={wizard.datosBasicos.carreraId}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, carreraId: value },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
disabled={!wizard.datosBasicos.facultadId}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="carrera"
|
||||||
|
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Selecciona carrera…" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{carrerasFiltradas.map((c) => (
|
||||||
|
<SelectItem key={c.id} value={c.id}>
|
||||||
|
{c.nombre}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="nivel">Nivel</Label>
|
||||||
|
<Select
|
||||||
|
value={wizard.datosBasicos.nivel}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: { ...w.datosBasicos, nivel: value },
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="nivel"
|
||||||
|
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Selecciona nivel…" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{NIVELES.map((n) => (
|
||||||
|
<SelectItem key={n} value={n}>
|
||||||
|
{n}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="tipoCiclo">Tipo de ciclo</Label>
|
||||||
|
<Select
|
||||||
|
value={wizard.datosBasicos.tipoCiclo}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
tipoCiclo: value as any,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="tipoCiclo"
|
||||||
|
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
|
||||||
|
>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{TIPOS_CICLO.map((t) => (
|
||||||
|
<SelectItem key={t.value} value={t.value}>
|
||||||
|
{t.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="numCiclos">Número de ciclos</Label>
|
||||||
|
<Input
|
||||||
|
id="numCiclos"
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={wizard.datosBasicos.numCiclos}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
numCiclos: Number(e.target.value || 1),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
323
src/components/planes/wizard/PasoDetallesPanel.tsx
Normal file
323
src/components/planes/wizard/PasoDetallesPanel.tsx
Normal file
@@ -0,0 +1,323 @@
|
|||||||
|
import type { NewPlanWizardState } from '@/features/planes/new/types'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import {
|
||||||
|
CARRERAS,
|
||||||
|
FACULTADES,
|
||||||
|
PLANES_EXISTENTES,
|
||||||
|
} from '@/features/planes/new/catalogs'
|
||||||
|
|
||||||
|
export function PasoDetallesPanel({
|
||||||
|
wizard,
|
||||||
|
onChange,
|
||||||
|
onGenerarIA,
|
||||||
|
isLoading,
|
||||||
|
}: {
|
||||||
|
wizard: NewPlanWizardState
|
||||||
|
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
|
||||||
|
onGenerarIA: () => void
|
||||||
|
isLoading: boolean
|
||||||
|
}) {
|
||||||
|
if (wizard.modoCreacion === 'MANUAL') {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Creación manual</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Se creará un plan en blanco con estructura mínima.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wizard.modoCreacion === 'IA') {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="desc">Descripción del enfoque</Label>
|
||||||
|
<textarea
|
||||||
|
id="desc"
|
||||||
|
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring min-h-24 w-full rounded-md border px-3 py-2 text-sm shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
|
placeholder="Describe el enfoque del programa…"
|
||||||
|
value={wizard.iaConfig?.descripcionEnfoque || ''}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
iaConfig: {
|
||||||
|
...(w.iaConfig || ({} as any)),
|
||||||
|
descripcionEnfoque: e.target.value,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="poblacion">Población objetivo</Label>
|
||||||
|
<Input
|
||||||
|
id="poblacion"
|
||||||
|
placeholder="Ej. Egresados de bachillerato con perfil STEM"
|
||||||
|
value={wizard.iaConfig?.poblacionObjetivo || ''}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
iaConfig: {
|
||||||
|
...(w.iaConfig || ({} as any)),
|
||||||
|
poblacionObjetivo: e.target.value,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="notas">Notas adicionales</Label>
|
||||||
|
<textarea
|
||||||
|
id="notas"
|
||||||
|
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring min-h-24 w-full rounded-md border px-3 py-2 text-sm shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
|
placeholder="Lineamientos institucionales, restricciones, etc."
|
||||||
|
value={wizard.iaConfig?.notasAdicionales || ''}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
iaConfig: {
|
||||||
|
...(w.iaConfig || ({} as any)),
|
||||||
|
notasAdicionales: e.target.value,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-muted-foreground text-sm">
|
||||||
|
Opcional: se pueden adjuntar recursos IA más adelante.
|
||||||
|
</div>
|
||||||
|
<Button onClick={onGenerarIA} disabled={isLoading}>
|
||||||
|
{isLoading ? 'Generando…' : 'Generar borrador con IA'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{wizard.resumen.previewPlan && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Preview IA</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Asignaturas aprox.:{' '}
|
||||||
|
{wizard.resumen.previewPlan.numAsignaturasAprox}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<ul className="text-muted-foreground list-disc pl-5 text-sm">
|
||||||
|
{wizard.resumen.previewPlan.secciones?.map((s) => (
|
||||||
|
<li key={s.id}>
|
||||||
|
<span className="text-foreground font-medium">
|
||||||
|
{s.titulo}:
|
||||||
|
</span>{' '}
|
||||||
|
{s.resumen}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
wizard.modoCreacion === 'CLONADO' &&
|
||||||
|
wizard.subModoClonado === 'INTERNO'
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="clonFacultad">Facultad</Label>
|
||||||
|
<select
|
||||||
|
id="clonFacultad"
|
||||||
|
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring h-10 w-full rounded-md border px-3 text-sm shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
|
value={wizard.datosBasicos.facultadId}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
facultadId: e.target.value,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="">Todas</option>
|
||||||
|
{FACULTADES.map((f) => (
|
||||||
|
<option key={f.id} value={f.id}>
|
||||||
|
{f.nombre}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="clonCarrera">Carrera</Label>
|
||||||
|
<select
|
||||||
|
id="clonCarrera"
|
||||||
|
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring h-10 w-full rounded-md border px-3 text-sm shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
|
value={wizard.datosBasicos.carreraId}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
carreraId: e.target.value,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="">Todas</option>
|
||||||
|
{CARRERAS.map((c) => (
|
||||||
|
<option key={c.id} value={c.id}>
|
||||||
|
{c.nombre}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="buscarPlan">Buscar</Label>
|
||||||
|
<Input
|
||||||
|
id="buscarPlan"
|
||||||
|
placeholder="Nombre del plan…"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const term = e.target.value.toLowerCase()
|
||||||
|
void term
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3">
|
||||||
|
{PLANES_EXISTENTES.filter(
|
||||||
|
(p) =>
|
||||||
|
(!wizard.datosBasicos.facultadId ||
|
||||||
|
p.facultadId === wizard.datosBasicos.facultadId) &&
|
||||||
|
(!wizard.datosBasicos.carreraId ||
|
||||||
|
p.carreraId === wizard.datosBasicos.carreraId),
|
||||||
|
).map((p) => (
|
||||||
|
<Card
|
||||||
|
key={p.id}
|
||||||
|
className={
|
||||||
|
p.id === wizard.clonInterno?.planOrigenId
|
||||||
|
? 'ring-ring ring-2'
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
onClick={() =>
|
||||||
|
onChange((w) => ({ ...w, clonInterno: { planOrigenId: p.id } }))
|
||||||
|
}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center justify-between">
|
||||||
|
<span>{p.nombre}</span>
|
||||||
|
<span className="text-muted-foreground text-sm">
|
||||||
|
{p.estado} · {p.anio}
|
||||||
|
</span>
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>ID: {p.id}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
wizard.modoCreacion === 'CLONADO' &&
|
||||||
|
wizard.subModoClonado === 'TRADICIONAL'
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="word">Word del plan (obligatorio)</Label>
|
||||||
|
<input
|
||||||
|
id="word"
|
||||||
|
type="file"
|
||||||
|
accept=".doc,.docx"
|
||||||
|
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring file:bg-secondary block w-full rounded-md border px-3 py-2 text-sm shadow-sm file:mr-4 file:rounded-md file:border-0 file:px-3 file:py-1.5 file:text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
clonTradicional: {
|
||||||
|
...(w.clonTradicional || ({} as any)),
|
||||||
|
archivoWordPlanId: e.target.files?.[0]
|
||||||
|
? `file_${e.target.files[0].name}`
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="mapa">Excel del mapa curricular</Label>
|
||||||
|
<input
|
||||||
|
id="mapa"
|
||||||
|
type="file"
|
||||||
|
accept=".xls,.xlsx"
|
||||||
|
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring file:bg-secondary block w-full rounded-md border px-3 py-2 text-sm shadow-sm file:mr-4 file:rounded-md file:border-0 file:px-3 file:py-1.5 file:text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
clonTradicional: {
|
||||||
|
...(w.clonTradicional || ({} as any)),
|
||||||
|
archivoMapaExcelId: e.target.files?.[0]
|
||||||
|
? `file_${e.target.files[0].name}`
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="asignaturas">Excel/listado de asignaturas</Label>
|
||||||
|
<input
|
||||||
|
id="asignaturas"
|
||||||
|
type="file"
|
||||||
|
accept=".xls,.xlsx,.csv"
|
||||||
|
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring file:bg-secondary block w-full rounded-md border px-3 py-2 text-sm shadow-sm file:mr-4 file:rounded-md file:border-0 file:px-3 file:py-1.5 file:text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
clonTradicional: {
|
||||||
|
...(w.clonTradicional || ({} as any)),
|
||||||
|
archivoAsignaturasExcelId: e.target.files?.[0]
|
||||||
|
? `file_${e.target.files[0].name}`
|
||||||
|
: null,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-muted-foreground text-sm">
|
||||||
|
Sube al menos Word y uno de los Excel para continuar.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Selecciona un modo</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Elige una opción en el paso anterior para continuar.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
122
src/components/planes/wizard/PasoModoCardGroup.tsx
Normal file
122
src/components/planes/wizard/PasoModoCardGroup.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import * as Icons from 'lucide-react'
|
||||||
|
|
||||||
|
import type {
|
||||||
|
NewPlanWizardState,
|
||||||
|
ModoCreacion,
|
||||||
|
SubModoClonado,
|
||||||
|
} from '@/features/planes/new/types'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
|
||||||
|
export function PasoModoCardGroup({
|
||||||
|
wizard,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
wizard: NewPlanWizardState
|
||||||
|
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
|
||||||
|
}) {
|
||||||
|
const isSelected = (m: ModoCreacion) => wizard.modoCreacion === m
|
||||||
|
const isSubSelected = (s: SubModoClonado) => wizard.subModoClonado === s
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4 sm:grid-cols-3">
|
||||||
|
<Card
|
||||||
|
className={isSelected('MANUAL') ? 'ring-ring ring-2' : ''}
|
||||||
|
onClick={() =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
modoCreacion: 'MANUAL',
|
||||||
|
subModoClonado: undefined,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Icons.Pencil className="text-primary h-5 w-5" /> Manual
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Plan vacío con estructura mínima.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className={isSelected('IA') ? 'ring-ring ring-2' : ''}
|
||||||
|
onClick={() =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
modoCreacion: 'IA',
|
||||||
|
subModoClonado: undefined,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Icons.Sparkles className="text-primary h-5 w-5" /> Con IA
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Borrador completo a partir de datos base.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
className={isSelected('CLONADO') ? 'ring-ring ring-2' : ''}
|
||||||
|
onClick={() => onChange((w) => ({ ...w, modoCreacion: 'CLONADO' }))}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Icons.Copy className="text-primary h-5 w-5" /> Clonado
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>Desde un plan existente o archivos.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
{wizard.modoCreacion === 'CLONADO' && (
|
||||||
|
<CardContent className="flex flex-col gap-3">
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onChange((w) => ({ ...w, subModoClonado: 'INTERNO' }))
|
||||||
|
}}
|
||||||
|
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer flex-row items-center justify-center gap-2 rounded-lg border p-4 text-center transition-all sm:flex-col ${
|
||||||
|
isSubSelected('INTERNO')
|
||||||
|
? 'border-primary bg-primary/5 ring-primary text-primary ring-1'
|
||||||
|
: 'border-border text-muted-foreground'
|
||||||
|
} `}
|
||||||
|
>
|
||||||
|
<Icons.Database className="mb-1 h-6 w-6" />
|
||||||
|
<span className="text-sm font-medium">Del sistema</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onChange((w) => ({ ...w, subModoClonado: 'TRADICIONAL' }))
|
||||||
|
}}
|
||||||
|
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer flex-row items-center justify-center gap-2 rounded-lg border p-4 text-center transition-all sm:flex-col ${
|
||||||
|
isSubSelected('TRADICIONAL')
|
||||||
|
? 'border-primary bg-primary/5 ring-primary text-primary ring-1'
|
||||||
|
: 'border-border text-muted-foreground'
|
||||||
|
} `}
|
||||||
|
>
|
||||||
|
<Icons.Upload className="mb-1 h-6 w-6" />
|
||||||
|
<span className="text-sm font-medium">Desde archivos</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
75
src/components/planes/wizard/PasoResumenCard.tsx
Normal file
75
src/components/planes/wizard/PasoResumenCard.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import type { NewPlanWizardState } from '@/features/planes/new/types'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
|
||||||
|
export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
||||||
|
const modo = wizard.modoCreacion
|
||||||
|
const sub = wizard.subModoClonado
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Resumen</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Verifica la información antes de crear.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="grid gap-2 text-sm">
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Nombre: </span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{wizard.datosBasicos.nombrePlan || '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Facultad/Carrera: </span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{wizard.datosBasicos.facultadId || '—'} /{' '}
|
||||||
|
{wizard.datosBasicos.carreraId || '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Nivel: </span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{wizard.datosBasicos.nivel || '—'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-muted-foreground">Ciclos: </span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{wizard.datosBasicos.numCiclos} ({wizard.datosBasicos.tipoCiclo})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
<span className="text-muted-foreground">Modo: </span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{modo === 'MANUAL' && 'Manual'}
|
||||||
|
{modo === 'IA' && 'Generado con IA'}
|
||||||
|
{modo === 'CLONADO' &&
|
||||||
|
sub === 'INTERNO' &&
|
||||||
|
'Clonado desde plan del sistema'}
|
||||||
|
{modo === 'CLONADO' &&
|
||||||
|
sub === 'TRADICIONAL' &&
|
||||||
|
'Importado desde documentos tradicionales'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{wizard.resumen.previewPlan && (
|
||||||
|
<div className="bg-muted mt-2 rounded-md p-3">
|
||||||
|
<div className="font-medium">Preview IA</div>
|
||||||
|
<div className="text-muted-foreground">
|
||||||
|
Asignaturas aprox.:{' '}
|
||||||
|
{wizard.resumen.previewPlan.numAsignaturasAprox}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
41
src/components/planes/wizard/StepWithTooltip.tsx
Normal file
41
src/components/planes/wizard/StepWithTooltip.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/components/ui/tooltip'
|
||||||
|
|
||||||
|
export function StepWithTooltip({
|
||||||
|
title,
|
||||||
|
desc,
|
||||||
|
}: {
|
||||||
|
title: string
|
||||||
|
desc: string
|
||||||
|
}) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip open={isOpen} onOpenChange={setIsOpen}>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span
|
||||||
|
className="cursor-help decoration-dotted underline-offset-4 hover:underline"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
setIsOpen((prev) => !prev)
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => setIsOpen(true)}
|
||||||
|
onMouseLeave={() => setIsOpen(false)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent className="max-w-[200px] text-xs">
|
||||||
|
<p>{desc}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
47
src/components/planes/wizard/WizardControls.tsx
Normal file
47
src/components/planes/wizard/WizardControls.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
export function WizardControls({
|
||||||
|
errorMessage,
|
||||||
|
onPrev,
|
||||||
|
onNext,
|
||||||
|
onCreate,
|
||||||
|
disablePrev,
|
||||||
|
disableNext,
|
||||||
|
disableCreate,
|
||||||
|
isLastStep,
|
||||||
|
}: {
|
||||||
|
errorMessage?: string | null
|
||||||
|
onPrev: () => void
|
||||||
|
onNext: () => void
|
||||||
|
onCreate: () => void
|
||||||
|
disablePrev: boolean
|
||||||
|
disableNext: boolean
|
||||||
|
disableCreate: boolean
|
||||||
|
isLastStep: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
{errorMessage && (
|
||||||
|
<span className="text-destructive text-sm font-medium">
|
||||||
|
{errorMessage}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button variant="secondary" onClick={onPrev} disabled={disablePrev}>
|
||||||
|
Anterior
|
||||||
|
</Button>
|
||||||
|
{isLastStep ? (
|
||||||
|
<Button onClick={onCreate} disabled={disableCreate}>
|
||||||
|
Crear plan
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button onClick={onNext} disabled={disableNext}>
|
||||||
|
Siguiente
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
78
src/components/planes/wizard/WizardHeader.tsx
Normal file
78
src/components/planes/wizard/WizardHeader.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import * as Icons from 'lucide-react'
|
||||||
|
|
||||||
|
import { StepWithTooltip } from './StepWithTooltip'
|
||||||
|
|
||||||
|
import { CircularProgress } from '@/components/CircularProgress'
|
||||||
|
import { DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
|
|
||||||
|
export function WizardHeader({
|
||||||
|
currentIndex,
|
||||||
|
totalSteps,
|
||||||
|
currentTitle,
|
||||||
|
currentDescription,
|
||||||
|
nextTitle,
|
||||||
|
onClose,
|
||||||
|
Wizard,
|
||||||
|
}: {
|
||||||
|
currentIndex: number
|
||||||
|
totalSteps: number
|
||||||
|
currentTitle: string
|
||||||
|
currentDescription: string
|
||||||
|
nextTitle?: string
|
||||||
|
onClose: () => void
|
||||||
|
Wizard: any
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="z-10 flex-none border-b bg-white">
|
||||||
|
<div className="flex items-center justify-between p-6 pb-4">
|
||||||
|
<DialogHeader className="p-0">
|
||||||
|
<DialogTitle>Nuevo plan de estudios</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none"
|
||||||
|
>
|
||||||
|
<Icons.X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Cerrar</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="px-6 pb-6">
|
||||||
|
<div className="block sm:hidden">
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<CircularProgress current={currentIndex} total={totalSteps} />
|
||||||
|
<div className="flex flex-col justify-center">
|
||||||
|
<h2 className="text-lg font-bold text-slate-900">
|
||||||
|
<StepWithTooltip
|
||||||
|
title={currentTitle}
|
||||||
|
desc={currentDescription}
|
||||||
|
/>
|
||||||
|
</h2>
|
||||||
|
{nextTitle ? (
|
||||||
|
<p className="text-sm text-slate-400">Siguiente: {nextTitle}</p>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm font-medium text-green-500">
|
||||||
|
¡Último paso!
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="hidden sm:block">
|
||||||
|
<Wizard.Stepper.Navigation className="border-border/60 rounded-xl border bg-slate-50 p-2">
|
||||||
|
{Wizard.steps.map((step: any) => (
|
||||||
|
<Wizard.Stepper.Step
|
||||||
|
key={step.id}
|
||||||
|
of={step.id}
|
||||||
|
className="whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<Wizard.Stepper.Title>
|
||||||
|
<StepWithTooltip title={step.title} desc={step.description} />
|
||||||
|
</Wizard.Stepper.Title>
|
||||||
|
</Wizard.Stepper.Step>
|
||||||
|
))}
|
||||||
|
</Wizard.Stepper.Navigation>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
536
src/components/stepper.tsx
Normal file
536
src/components/stepper.tsx
Normal file
@@ -0,0 +1,536 @@
|
|||||||
|
import { Slot } from '@radix-ui/react-slot'
|
||||||
|
import * as Stepperize from '@stepperize/react'
|
||||||
|
import { cva } from 'class-variance-authority'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import type { VariantProps } from 'class-variance-authority'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const StepperContext = React.createContext<Stepper.ConfigProps | null>(null)
|
||||||
|
|
||||||
|
const useStepperProvider = (): Stepper.ConfigProps => {
|
||||||
|
const context = React.useContext(StepperContext)
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useStepper must be used within a StepperProvider.')
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
|
|
||||||
|
const defineStepper = <const Steps extends Array<Stepperize.Step>>(
|
||||||
|
...steps: Steps
|
||||||
|
): Stepper.DefineProps<Steps> => {
|
||||||
|
const { Scoped, useStepper, ...rest } = Stepperize.defineStepper(...steps)
|
||||||
|
|
||||||
|
const StepperContainer = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: Omit<React.ComponentProps<'div'>, 'children'> & {
|
||||||
|
children:
|
||||||
|
| React.ReactNode
|
||||||
|
| ((props: { methods: Stepperize.Stepper<Steps> }) => React.ReactNode)
|
||||||
|
}) => {
|
||||||
|
const methods = useStepper()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
date-component="stepper"
|
||||||
|
className={cn('w-full', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{typeof children === 'function' ? children({ methods }) : children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rest,
|
||||||
|
useStepper,
|
||||||
|
Stepper: {
|
||||||
|
Provider: ({
|
||||||
|
variant = 'horizontal',
|
||||||
|
labelOrientation = 'horizontal',
|
||||||
|
tracking = false,
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
// Avoid leaking non-DOM props like `initialStep` onto the div
|
||||||
|
const { initialStep, initialMetadata, ...restProps } = props as {
|
||||||
|
initialStep?: any
|
||||||
|
initialMetadata?: any
|
||||||
|
} & Record<string, unknown>
|
||||||
|
return (
|
||||||
|
<StepperContext.Provider
|
||||||
|
value={{ variant, labelOrientation, tracking }}
|
||||||
|
>
|
||||||
|
<Scoped initialStep={initialStep} initialMetadata={initialMetadata}>
|
||||||
|
<StepperContainer className={className} {...(restProps as any)}>
|
||||||
|
{children}
|
||||||
|
</StepperContainer>
|
||||||
|
</Scoped>
|
||||||
|
</StepperContext.Provider>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
Navigation: ({
|
||||||
|
children,
|
||||||
|
'aria-label': ariaLabel = 'Stepper Navigation',
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const { variant } = useStepperProvider()
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
date-component="stepper-navigation"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
role="tablist"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ol
|
||||||
|
date-component="stepper-navigation-list"
|
||||||
|
className={classForNavigationList({ variant: variant })}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
Step: ({ children, className, icon, ...props }) => {
|
||||||
|
const { variant, labelOrientation } = useStepperProvider()
|
||||||
|
const { current } = useStepper()
|
||||||
|
|
||||||
|
const utils = rest.utils
|
||||||
|
const steps = rest.steps
|
||||||
|
|
||||||
|
const stepIndex = utils.getIndex(props.of)
|
||||||
|
const step = steps[stepIndex]
|
||||||
|
const currentIndex = utils.getIndex(current.id)
|
||||||
|
|
||||||
|
const isLast = utils.getLast().id === props.of
|
||||||
|
const isActive = current.id === props.of
|
||||||
|
|
||||||
|
const dataState = getStepState(currentIndex, stepIndex)
|
||||||
|
const childMap = useStepChildren(children)
|
||||||
|
|
||||||
|
const title = childMap.get('title')
|
||||||
|
const description = childMap.get('description')
|
||||||
|
const panel = childMap.get('panel')
|
||||||
|
|
||||||
|
if (variant === 'circle') {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
date-component="stepper-step"
|
||||||
|
className={cn(
|
||||||
|
'flex shrink-0 items-center gap-4 rounded-md transition-colors',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CircleStepIndicator
|
||||||
|
currentStep={stepIndex + 1}
|
||||||
|
totalSteps={steps.length}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
date-component="stepper-step-content"
|
||||||
|
className="flex flex-col items-start gap-1"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<li
|
||||||
|
date-component="stepper-step"
|
||||||
|
className={cn([
|
||||||
|
'group peer relative flex items-center gap-2',
|
||||||
|
'data-[variant=vertical]:flex-row',
|
||||||
|
'data-[label-orientation=vertical]:w-full',
|
||||||
|
'data-[label-orientation=vertical]:flex-col',
|
||||||
|
'data-[label-orientation=vertical]:justify-center',
|
||||||
|
])}
|
||||||
|
data-variant={variant}
|
||||||
|
data-label-orientation={labelOrientation}
|
||||||
|
data-state={dataState}
|
||||||
|
data-disabled={props.disabled}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
id={`step-${step.id}`}
|
||||||
|
date-component="stepper-step-indicator"
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
tabIndex={dataState !== 'inactive' ? 0 : -1}
|
||||||
|
className="rounded-full"
|
||||||
|
variant={dataState !== 'inactive' ? 'default' : 'secondary'}
|
||||||
|
size="icon"
|
||||||
|
aria-controls={`step-panel-${props.of}`}
|
||||||
|
aria-current={isActive ? 'step' : undefined}
|
||||||
|
aria-posinset={stepIndex + 1}
|
||||||
|
aria-setsize={steps.length}
|
||||||
|
aria-selected={isActive}
|
||||||
|
onKeyDown={(e) =>
|
||||||
|
onStepKeyDown(
|
||||||
|
e,
|
||||||
|
utils.getNext(props.of),
|
||||||
|
utils.getPrev(props.of),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{icon ?? stepIndex + 1}
|
||||||
|
</Button>
|
||||||
|
{variant === 'horizontal' && labelOrientation === 'vertical' && (
|
||||||
|
<StepperSeparator
|
||||||
|
orientation="horizontal"
|
||||||
|
labelOrientation={labelOrientation}
|
||||||
|
isLast={isLast}
|
||||||
|
state={dataState}
|
||||||
|
disabled={props.disabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
date-component="stepper-step-content"
|
||||||
|
className="flex flex-col items-start"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{variant === 'horizontal' && labelOrientation === 'horizontal' && (
|
||||||
|
<StepperSeparator
|
||||||
|
orientation="horizontal"
|
||||||
|
isLast={isLast}
|
||||||
|
state={dataState}
|
||||||
|
disabled={props.disabled}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{variant === 'vertical' && (
|
||||||
|
<div className="flex gap-4">
|
||||||
|
{!isLast && (
|
||||||
|
<div className="flex justify-center ps-[calc(var(--spacing)_*_4.5_-_1px)]">
|
||||||
|
<StepperSeparator
|
||||||
|
orientation="vertical"
|
||||||
|
isLast={isLast}
|
||||||
|
state={dataState}
|
||||||
|
disabled={props.disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="my-3 flex-1 ps-4">{panel}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
Title,
|
||||||
|
Description,
|
||||||
|
Panel: ({ children, asChild, ...props }) => {
|
||||||
|
const Comp = asChild ? Slot : 'div'
|
||||||
|
const { tracking } = useStepperProvider()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
date-component="stepper-step-panel"
|
||||||
|
ref={(node) => scrollIntoStepperPanel(node, tracking)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Comp>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
Controls: ({ children, className, asChild, ...props }) => {
|
||||||
|
const Comp = asChild ? Slot : 'div'
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
date-component="stepper-controls"
|
||||||
|
className={cn('flex justify-end gap-4', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Comp>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Title = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
asChild,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'h4'> & { asChild?: boolean }) => {
|
||||||
|
const Comp = asChild ? Slot : 'h4'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
date-component="stepper-step-title"
|
||||||
|
className={cn('text-base font-medium', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Comp>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Description = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
asChild,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<'p'> & { asChild?: boolean }) => {
|
||||||
|
const Comp = asChild ? Slot : 'p'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
date-component="stepper-step-description"
|
||||||
|
className={cn('text-muted-foreground text-sm', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Comp>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepperSeparator = ({
|
||||||
|
orientation,
|
||||||
|
isLast,
|
||||||
|
labelOrientation,
|
||||||
|
state,
|
||||||
|
disabled,
|
||||||
|
}: {
|
||||||
|
isLast: boolean
|
||||||
|
state: string
|
||||||
|
disabled?: boolean
|
||||||
|
} & VariantProps<typeof classForSeparator>) => {
|
||||||
|
if (isLast) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
date-component="stepper-separator"
|
||||||
|
data-orientation={orientation}
|
||||||
|
data-state={state}
|
||||||
|
data-disabled={disabled}
|
||||||
|
role="separator"
|
||||||
|
tabIndex={-1}
|
||||||
|
className={classForSeparator({ orientation, labelOrientation })}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CircleStepIndicator = ({
|
||||||
|
currentStep,
|
||||||
|
totalSteps,
|
||||||
|
size = 80,
|
||||||
|
strokeWidth = 6,
|
||||||
|
}: Stepper.CircleStepIndicatorProps) => {
|
||||||
|
const radius = (size - strokeWidth) / 2
|
||||||
|
const circumference = radius * 2 * Math.PI
|
||||||
|
const fillPercentage = (currentStep / totalSteps) * 100
|
||||||
|
const dashOffset = circumference - (circumference * fillPercentage) / 100
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
date-component="stepper-step-indicator"
|
||||||
|
role="progressbar"
|
||||||
|
aria-valuenow={currentStep}
|
||||||
|
aria-valuemin={1}
|
||||||
|
aria-valuemax={totalSteps}
|
||||||
|
tabIndex={-1}
|
||||||
|
className="relative inline-flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<svg width={size} height={size}>
|
||||||
|
<title>Step Indicator</title>
|
||||||
|
<circle
|
||||||
|
cx={size / 2}
|
||||||
|
cy={size / 2}
|
||||||
|
r={radius}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
className="text-muted-foreground"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx={size / 2}
|
||||||
|
cy={size / 2}
|
||||||
|
r={radius}
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
strokeDasharray={circumference}
|
||||||
|
strokeDashoffset={dashOffset}
|
||||||
|
className="text-primary transition-all duration-300 ease-in-out"
|
||||||
|
transform={`rotate(-90 ${size / 2} ${size / 2})`}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
|
<span className="text-sm font-medium" aria-live="polite">
|
||||||
|
{currentStep} of {totalSteps}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const classForNavigationList = cva('flex gap-2', {
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
horizontal: 'flex-row items-center justify-between',
|
||||||
|
vertical: 'flex-col',
|
||||||
|
circle: 'flex-row items-center justify-between',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const classForSeparator = cva(
|
||||||
|
[
|
||||||
|
'bg-muted',
|
||||||
|
'data-[state=completed]:bg-primary data-[disabled]:opacity-50',
|
||||||
|
'transition-all duration-300 ease-in-out',
|
||||||
|
],
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
orientation: {
|
||||||
|
horizontal: 'h-0.5 flex-1',
|
||||||
|
vertical: 'h-full w-0.5',
|
||||||
|
},
|
||||||
|
labelOrientation: {
|
||||||
|
vertical:
|
||||||
|
'absolute top-5 right-[calc(-50%+20px)] left-[calc(50%+30px)] block shrink-0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
function scrollIntoStepperPanel(
|
||||||
|
node: HTMLDivElement | null,
|
||||||
|
tracking?: boolean,
|
||||||
|
) {
|
||||||
|
if (tracking) {
|
||||||
|
node?.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const useStepChildren = (children: React.ReactNode) => {
|
||||||
|
return React.useMemo(() => extractChildren(children), [children])
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractChildren = (children: React.ReactNode) => {
|
||||||
|
const childrenArray = React.Children.toArray(children)
|
||||||
|
const map = new Map<string, React.ReactNode>()
|
||||||
|
|
||||||
|
for (const child of childrenArray) {
|
||||||
|
if (React.isValidElement(child)) {
|
||||||
|
if (child.type === Title) {
|
||||||
|
map.set('title', child)
|
||||||
|
} else if (child.type === Description) {
|
||||||
|
map.set('description', child)
|
||||||
|
} else {
|
||||||
|
map.set('panel', child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
const onStepKeyDown = (
|
||||||
|
e: React.KeyboardEvent<HTMLButtonElement>,
|
||||||
|
nextStep: Stepperize.Step,
|
||||||
|
prevStep: Stepperize.Step,
|
||||||
|
) => {
|
||||||
|
const { key } = e
|
||||||
|
const directions = {
|
||||||
|
next: ['ArrowRight', 'ArrowDown'],
|
||||||
|
prev: ['ArrowLeft', 'ArrowUp'],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directions.next.includes(key) || directions.prev.includes(key)) {
|
||||||
|
const direction = directions.next.includes(key) ? 'next' : 'prev'
|
||||||
|
const step = direction === 'next' ? nextStep : prevStep
|
||||||
|
|
||||||
|
if (!step) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const stepElement = document.getElementById(`step-${step.id}`)
|
||||||
|
if (!stepElement) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isActive =
|
||||||
|
stepElement.parentElement?.getAttribute('data-state') !== 'inactive'
|
||||||
|
if (isActive || direction === 'prev') {
|
||||||
|
stepElement.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStepState = (currentIndex: number, stepIndex: number) => {
|
||||||
|
if (currentIndex === stepIndex) {
|
||||||
|
return 'active'
|
||||||
|
}
|
||||||
|
if (currentIndex > stepIndex) {
|
||||||
|
return 'completed'
|
||||||
|
}
|
||||||
|
return 'inactive'
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Stepper {
|
||||||
|
export type StepperVariant = 'horizontal' | 'vertical' | 'circle'
|
||||||
|
export type StepperLabelOrientation = 'horizontal' | 'vertical'
|
||||||
|
|
||||||
|
export type ConfigProps = {
|
||||||
|
variant?: StepperVariant
|
||||||
|
labelOrientation?: StepperLabelOrientation
|
||||||
|
tracking?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DefineProps<Steps extends Array<Stepperize.Step>> = Omit<
|
||||||
|
Stepperize.StepperReturn<Steps>,
|
||||||
|
'Scoped'
|
||||||
|
> & {
|
||||||
|
Stepper: {
|
||||||
|
Provider: (
|
||||||
|
props: Omit<Stepperize.ScopedProps<Steps>, 'children'> &
|
||||||
|
Omit<React.ComponentProps<'div'>, 'children'> &
|
||||||
|
Stepper.ConfigProps & {
|
||||||
|
children:
|
||||||
|
| React.ReactNode
|
||||||
|
| ((props: {
|
||||||
|
methods: Stepperize.Stepper<Steps>
|
||||||
|
}) => React.ReactNode)
|
||||||
|
},
|
||||||
|
) => React.ReactElement
|
||||||
|
Navigation: (props: React.ComponentProps<'nav'>) => React.ReactElement
|
||||||
|
Step: (
|
||||||
|
props: React.ComponentProps<'button'> & {
|
||||||
|
of: Stepperize.Get.Id<Steps>
|
||||||
|
icon?: React.ReactNode
|
||||||
|
},
|
||||||
|
) => React.ReactElement
|
||||||
|
Title: (props: AsChildProps<'h4'>) => React.ReactElement
|
||||||
|
Description: (props: AsChildProps<'p'>) => React.ReactElement
|
||||||
|
Panel: (props: AsChildProps<'div'>) => React.ReactElement
|
||||||
|
Controls: (props: AsChildProps<'div'>) => React.ReactElement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CircleStepIndicatorProps = {
|
||||||
|
currentStep: number
|
||||||
|
totalSteps: number
|
||||||
|
size?: number
|
||||||
|
strokeWidth?: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AsChildProps<T extends React.ElementType> = React.ComponentProps<T> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export { defineStepper }
|
||||||
@@ -1,28 +1,21 @@
|
|||||||
interface InputProps {
|
import * as React from "react"
|
||||||
label: string
|
|
||||||
value: string
|
|
||||||
onChange: (value: string) => void
|
|
||||||
type?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Input({
|
import { cn } from "@/lib/utils"
|
||||||
label,
|
|
||||||
value,
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
onChange,
|
|
||||||
type = 'text',
|
|
||||||
}: InputProps) {
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-1">
|
<input
|
||||||
<label className="text-sm font-medium text-gray-700">
|
type={type}
|
||||||
{label}
|
data-slot="input"
|
||||||
</label>
|
className={cn(
|
||||||
<input
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
type={type}
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
value={value}
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
onChange={e => onChange(e.target.value)}
|
className
|
||||||
className="w-full rounded-lg border border-gray-300 px-3 py-2
|
)}
|
||||||
focus:outline-none focus:ring-2 focus:ring-[#7b0f1d]"
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { Input }
|
||||||
|
|||||||
25
src/components/ui/LoginInput.tsx
Normal file
25
src/components/ui/LoginInput.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
interface InputProps {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
onChange: (value: string) => void
|
||||||
|
type?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoginInput({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
type = 'text',
|
||||||
|
}: InputProps) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm font-medium text-gray-700">{label}</label>
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
className="w-full rounded-lg border border-gray-300 px-3 py-2 focus:ring-2 focus:ring-[#7b0f1d] focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
62
src/components/ui/button.tsx
Normal file
62
src/components/ui/button.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
"icon-sm": "size-8",
|
||||||
|
"icon-lg": "size-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant = "default",
|
||||||
|
size = "default",
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
data-variant={variant}
|
||||||
|
data-size={size}
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
182
src/components/ui/command.tsx
Normal file
182
src/components/ui/command.tsx
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Command as CommandPrimitive } from "cmdk"
|
||||||
|
import { SearchIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog"
|
||||||
|
|
||||||
|
function Command({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive
|
||||||
|
data-slot="command"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandDialog({
|
||||||
|
title = "Command Palette",
|
||||||
|
description = "Search for a command to run...",
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Dialog> & {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
className?: string
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogDescription>{description}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogContent
|
||||||
|
className={cn("overflow-hidden p-0", className)}
|
||||||
|
showCloseButton={showCloseButton}
|
||||||
|
>
|
||||||
|
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
|
{children}
|
||||||
|
</Command>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="command-input-wrapper"
|
||||||
|
className="flex h-9 items-center gap-2 border-b px-3"
|
||||||
|
>
|
||||||
|
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
data-slot="command-input"
|
||||||
|
className={cn(
|
||||||
|
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
data-slot="command-list"
|
||||||
|
className={cn(
|
||||||
|
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandEmpty({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Empty
|
||||||
|
data-slot="command-empty"
|
||||||
|
className="py-6 text-center text-sm"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
data-slot="command-group"
|
||||||
|
className={cn(
|
||||||
|
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Separator
|
||||||
|
data-slot="command-separator"
|
||||||
|
className={cn("bg-border -mx-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
data-slot="command-item"
|
||||||
|
className={cn(
|
||||||
|
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="command-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandShortcut,
|
||||||
|
CommandSeparator,
|
||||||
|
}
|
||||||
143
src/components/ui/dialog.tsx
Normal file
143
src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { XIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Dialog({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||||
|
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||||
|
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogPortal({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||||
|
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogClose({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||||
|
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogOverlay({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
data-slot="dialog-overlay"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||||
|
showCloseButton?: boolean
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<DialogPortal data-slot="dialog-portal">
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
data-slot="dialog-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{showCloseButton && (
|
||||||
|
<DialogPrimitive.Close
|
||||||
|
data-slot="dialog-close"
|
||||||
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
)}
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-header"
|
||||||
|
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="dialog-footer"
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogTitle({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
data-slot="dialog-title"
|
||||||
|
className={cn("text-lg leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DialogDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||||
|
return (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
data-slot="dialog-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogPortal,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger,
|
||||||
|
}
|
||||||
22
src/components/ui/label.tsx
Normal file
22
src/components/ui/label.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Label({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
data-slot="label"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Label }
|
||||||
46
src/components/ui/popover.tsx
Normal file
46
src/components/ui/popover.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Popover({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||||
|
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||||
|
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverContent({
|
||||||
|
className,
|
||||||
|
align = "center",
|
||||||
|
sideOffset = 4,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
data-slot="popover-content"
|
||||||
|
align={align}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PopoverAnchor({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||||
|
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||||
188
src/components/ui/select.tsx
Normal file
188
src/components/ui/select.tsx
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Select({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
|
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||||
|
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectValue({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectTrigger({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
|
size?: "sm" | "default"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
data-slot="select-trigger"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDownIcon className="size-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
position = "item-aligned",
|
||||||
|
align = "center",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
data-slot="select-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
align={align}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
data-slot="select-label"
|
||||||
|
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
data-slot="select-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
data-slot="select-item-indicator"
|
||||||
|
className="absolute right-2 flex size-3.5 items-center justify-center"
|
||||||
|
>
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
data-slot="select-separator"
|
||||||
|
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollUpButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollDownButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
}
|
||||||
26
src/components/ui/separator.tsx
Normal file
26
src/components/ui/separator.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as SeparatorPrimitive from "@radix-ui/react-separator"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Separator({
|
||||||
|
className,
|
||||||
|
orientation = "horizontal",
|
||||||
|
decorative = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<SeparatorPrimitive.Root
|
||||||
|
data-slot="separator"
|
||||||
|
decorative={decorative}
|
||||||
|
orientation={orientation}
|
||||||
|
className={cn(
|
||||||
|
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Separator }
|
||||||
18
src/components/ui/textarea.tsx
Normal file
18
src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
data-slot="textarea"
|
||||||
|
className={cn(
|
||||||
|
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Textarea }
|
||||||
59
src/components/ui/tooltip.tsx
Normal file
59
src/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function TooltipProvider({
|
||||||
|
delayDuration = 0,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||||
|
return (
|
||||||
|
<TooltipPrimitive.Provider
|
||||||
|
data-slot="tooltip-provider"
|
||||||
|
delayDuration={delayDuration}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Tooltip({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||||
|
</TooltipProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipTrigger({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||||
|
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function TooltipContent({
|
||||||
|
className,
|
||||||
|
sideOffset = 0,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<TooltipPrimitive.Portal>
|
||||||
|
<TooltipPrimitive.Content
|
||||||
|
data-slot="tooltip-content"
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
|
||||||
|
</TooltipPrimitive.Content>
|
||||||
|
</TooltipPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||||
127
src/features/asignaturas/new/NuevaAsignaturaModalContainer.tsx
Normal file
127
src/features/asignaturas/new/NuevaAsignaturaModalContainer.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import { useNuevaAsignaturaWizard } from './hooks/useNuevaAsignaturaWizard'
|
||||||
|
|
||||||
|
import { PasoBasicosForm } from '@/components/asignaturas/wizard/PasoBasicosForm'
|
||||||
|
import { PasoConfiguracionPanel } from '@/components/asignaturas/wizard/PasoConfiguracionPanel'
|
||||||
|
import { PasoMetodoCardGroup } from '@/components/asignaturas/wizard/PasoMetodoCardGroup'
|
||||||
|
import { PasoResumenCard } from '@/components/asignaturas/wizard/PasoResumenCard'
|
||||||
|
import { VistaSinPermisos } from '@/components/asignaturas/wizard/VistaSinPermisos'
|
||||||
|
import { WizardControls } from '@/components/asignaturas/wizard/WizardControls'
|
||||||
|
import { WizardHeader } from '@/components/asignaturas/wizard/WizardHeader'
|
||||||
|
import { defineStepper } from '@/components/stepper'
|
||||||
|
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||||
|
|
||||||
|
const Wizard = defineStepper(
|
||||||
|
{
|
||||||
|
id: 'metodo',
|
||||||
|
title: 'Método',
|
||||||
|
description: 'Manual, IA o Clonado',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'basicos',
|
||||||
|
title: 'Datos básicos',
|
||||||
|
description: 'Nombre y estructura',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'configuracion',
|
||||||
|
title: 'Configuración',
|
||||||
|
description: 'Detalles según modo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'resumen',
|
||||||
|
title: 'Resumen',
|
||||||
|
description: 'Confirmar creación',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const auth_get_current_user_role = () => 'JEFE_CARRERA' as const
|
||||||
|
|
||||||
|
export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const role = auth_get_current_user_role()
|
||||||
|
|
||||||
|
const {
|
||||||
|
wizard,
|
||||||
|
setWizard,
|
||||||
|
canContinueDesdeMetodo,
|
||||||
|
canContinueDesdeBasicos,
|
||||||
|
canContinueDesdeConfig,
|
||||||
|
simularGeneracionIA,
|
||||||
|
crearAsignatura,
|
||||||
|
} = useNuevaAsignaturaWizard(planId)
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
navigate({ to: `/planes/${planId}/asignaturas`, resetScroll: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={true} onOpenChange={(open) => !open && handleClose()}>
|
||||||
|
<DialogContent
|
||||||
|
className="flex h-[90vh] w-[calc(100%-2rem)] flex-col gap-0 overflow-hidden p-0 sm:max-w-4xl"
|
||||||
|
onInteractOutside={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{role !== 'JEFE_CARRERA' ? (
|
||||||
|
<VistaSinPermisos onClose={handleClose} />
|
||||||
|
) : (
|
||||||
|
<Wizard.Stepper.Provider
|
||||||
|
initialStep={Wizard.utils.getFirst().id}
|
||||||
|
className="flex h-full flex-col"
|
||||||
|
>
|
||||||
|
{({ methods }) => (
|
||||||
|
<>
|
||||||
|
<WizardHeader
|
||||||
|
title="Nueva Asignatura"
|
||||||
|
Wizard={Wizard}
|
||||||
|
methods={{ ...methods, onClose: handleClose }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto bg-gray-50/30 p-6">
|
||||||
|
<div className="mx-auto max-w-3xl">
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 0 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoMetodoCardGroup
|
||||||
|
wizard={wizard}
|
||||||
|
onChange={setWizard}
|
||||||
|
/>
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 1 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoBasicosForm wizard={wizard} onChange={setWizard} />
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 2 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoConfiguracionPanel
|
||||||
|
wizard={wizard}
|
||||||
|
onChange={setWizard}
|
||||||
|
onGenerarIA={simularGeneracionIA}
|
||||||
|
/>
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 3 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoResumenCard wizard={wizard} />
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<WizardControls
|
||||||
|
Wizard={Wizard}
|
||||||
|
methods={methods}
|
||||||
|
wizard={wizard}
|
||||||
|
canContinueDesdeMetodo={canContinueDesdeMetodo}
|
||||||
|
canContinueDesdeBasicos={canContinueDesdeBasicos}
|
||||||
|
canContinueDesdeConfig={canContinueDesdeConfig}
|
||||||
|
onCreate={() => crearAsignatura(handleClose)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Wizard.Stepper.Provider>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
56
src/features/asignaturas/new/catalogs.ts
Normal file
56
src/features/asignaturas/new/catalogs.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import type { TipoAsignatura } from "./types";
|
||||||
|
|
||||||
|
export const ESTRUCTURAS_SEP = [
|
||||||
|
{ id: "sep-lic-2025", label: "Licenciatura SEP v2025" },
|
||||||
|
{ id: "sep-pos-2023", label: "Posgrado SEP v2023" },
|
||||||
|
{ id: "ulsa-int-2024", label: "Estándar Interno ULSA 2024" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TIPOS_MATERIA: Array<{ value: TipoAsignatura; label: string }> = [
|
||||||
|
{ value: "OBLIGATORIA", label: "Obligatoria" },
|
||||||
|
{ value: "OPTATIVA", label: "Optativa" },
|
||||||
|
{ value: "TRONCAL", label: "Troncal / Eje común" },
|
||||||
|
{ value: "OTRO", label: "Otro" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FACULTADES = [
|
||||||
|
{ id: "ing", nombre: "Facultad de Ingeniería" },
|
||||||
|
{ id: "med", nombre: "Facultad de Medicina" },
|
||||||
|
{ id: "neg", nombre: "Facultad de Negocios" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CARRERAS = [
|
||||||
|
{ id: "sis", nombre: "Ing. en Sistemas", facultadId: "ing" },
|
||||||
|
{ id: "ind", nombre: "Ing. Industrial", facultadId: "ing" },
|
||||||
|
{ id: "medico", nombre: "Médico Cirujano", facultadId: "med" },
|
||||||
|
{ id: "act", nombre: "Actuaría", facultadId: "neg" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PLANES_MOCK = [
|
||||||
|
{ id: "p1", nombre: "Plan 2010 Sistemas", carreraId: "sis" },
|
||||||
|
{ id: "p2", nombre: "Plan 2016 Sistemas", carreraId: "sis" },
|
||||||
|
{ id: "p3", nombre: "Plan 2015 Industrial", carreraId: "ind" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MATERIAS_MOCK = [
|
||||||
|
{
|
||||||
|
id: "m1",
|
||||||
|
nombre: "Programación Orientada a Objetos",
|
||||||
|
creditos: 8,
|
||||||
|
clave: "POO-101",
|
||||||
|
},
|
||||||
|
{ id: "m2", nombre: "Cálculo Diferencial", creditos: 6, clave: "MAT-101" },
|
||||||
|
{ id: "m3", nombre: "Ética Profesional", creditos: 4, clave: "HUM-302" },
|
||||||
|
{
|
||||||
|
id: "m4",
|
||||||
|
nombre: "Bases de Datos Avanzadas",
|
||||||
|
creditos: 8,
|
||||||
|
clave: "BD-201",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ARCHIVOS_SISTEMA_MOCK = [
|
||||||
|
{ id: "doc1", name: "Sílabo_Base_Ingenieria.pdf" },
|
||||||
|
{ id: "doc2", name: "Competencias_Egreso_2025.docx" },
|
||||||
|
{ id: "doc3", name: "Reglamento_Academico.pdf" },
|
||||||
|
];
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import type { AsignaturaPreview, NewSubjectWizardState } from "../types";
|
||||||
|
|
||||||
|
export function useNuevaAsignaturaWizard(planId: string) {
|
||||||
|
const [wizard, setWizard] = useState<NewSubjectWizardState>({
|
||||||
|
step: 1,
|
||||||
|
planId,
|
||||||
|
modoCreacion: null,
|
||||||
|
datosBasicos: {
|
||||||
|
nombre: "",
|
||||||
|
clave: "",
|
||||||
|
tipo: "OBLIGATORIA",
|
||||||
|
creditos: 0,
|
||||||
|
horasSemana: 0,
|
||||||
|
estructuraId: "",
|
||||||
|
},
|
||||||
|
clonInterno: {},
|
||||||
|
clonTradicional: {
|
||||||
|
archivoWordAsignaturaId: null,
|
||||||
|
archivosAdicionalesIds: [],
|
||||||
|
},
|
||||||
|
iaConfig: {
|
||||||
|
descripcionEnfoque: "",
|
||||||
|
notasAdicionales: "",
|
||||||
|
archivosExistentesIds: [],
|
||||||
|
},
|
||||||
|
resumen: {},
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const canContinueDesdeMetodo = wizard.modoCreacion === "MANUAL" ||
|
||||||
|
wizard.modoCreacion === "IA" ||
|
||||||
|
(wizard.modoCreacion === "CLONADO" && !!wizard.subModoClonado);
|
||||||
|
|
||||||
|
const canContinueDesdeBasicos = !!wizard.datosBasicos.nombre &&
|
||||||
|
wizard.datosBasicos.creditos > 0 &&
|
||||||
|
!!wizard.datosBasicos.estructuraId;
|
||||||
|
|
||||||
|
const canContinueDesdeConfig = (() => {
|
||||||
|
if (wizard.modoCreacion === "MANUAL") return true;
|
||||||
|
if (wizard.modoCreacion === "IA") {
|
||||||
|
return !!wizard.iaConfig?.descripcionEnfoque;
|
||||||
|
}
|
||||||
|
if (wizard.modoCreacion === "CLONADO") {
|
||||||
|
if (wizard.subModoClonado === "INTERNO") {
|
||||||
|
return !!wizard.clonInterno?.asignaturaOrigenId;
|
||||||
|
}
|
||||||
|
if (wizard.subModoClonado === "TRADICIONAL") {
|
||||||
|
return !!wizard.clonTradicional?.archivoWordAsignaturaId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const simularGeneracionIA = async () => {
|
||||||
|
setWizard((w) => ({ ...w, isLoading: true }));
|
||||||
|
await new Promise((r) => setTimeout(r, 1500));
|
||||||
|
setWizard((w) => ({
|
||||||
|
...w,
|
||||||
|
isLoading: false,
|
||||||
|
resumen: {
|
||||||
|
previewAsignatura: {
|
||||||
|
nombre: w.datosBasicos.nombre,
|
||||||
|
objetivo:
|
||||||
|
"Aplicar los fundamentos teóricos para la resolución de problemas...",
|
||||||
|
unidades: 5,
|
||||||
|
bibliografiaCount: 3,
|
||||||
|
} as AsignaturaPreview,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const crearAsignatura = async (onCreated: () => void) => {
|
||||||
|
setWizard((w) => ({ ...w, isLoading: true }));
|
||||||
|
await new Promise((r) => setTimeout(r, 1000));
|
||||||
|
onCreated();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
wizard,
|
||||||
|
setWizard,
|
||||||
|
canContinueDesdeMetodo,
|
||||||
|
canContinueDesdeBasicos,
|
||||||
|
canContinueDesdeConfig,
|
||||||
|
simularGeneracionIA,
|
||||||
|
crearAsignatura,
|
||||||
|
};
|
||||||
|
}
|
||||||
45
src/features/asignaturas/new/types.ts
Normal file
45
src/features/asignaturas/new/types.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
export type ModoCreacion = "MANUAL" | "IA" | "CLONADO";
|
||||||
|
export type SubModoClonado = "INTERNO" | "TRADICIONAL";
|
||||||
|
export type TipoAsignatura = "OBLIGATORIA" | "OPTATIVA" | "TRONCAL" | "OTRO";
|
||||||
|
|
||||||
|
export type AsignaturaPreview = {
|
||||||
|
nombre: string;
|
||||||
|
objetivo: string;
|
||||||
|
unidades: number;
|
||||||
|
bibliografiaCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NewSubjectWizardState = {
|
||||||
|
step: 1 | 2 | 3 | 4;
|
||||||
|
planId: string;
|
||||||
|
modoCreacion: ModoCreacion | null;
|
||||||
|
subModoClonado?: SubModoClonado;
|
||||||
|
datosBasicos: {
|
||||||
|
nombre: string;
|
||||||
|
clave?: string;
|
||||||
|
tipo: TipoAsignatura;
|
||||||
|
creditos: number;
|
||||||
|
horasSemana?: number;
|
||||||
|
estructuraId: string;
|
||||||
|
};
|
||||||
|
clonInterno?: {
|
||||||
|
facultadId?: string;
|
||||||
|
carreraId?: string;
|
||||||
|
planOrigenId?: string;
|
||||||
|
asignaturaOrigenId?: string | null;
|
||||||
|
};
|
||||||
|
clonTradicional?: {
|
||||||
|
archivoWordAsignaturaId: string | null;
|
||||||
|
archivosAdicionalesIds: Array<string>;
|
||||||
|
};
|
||||||
|
iaConfig?: {
|
||||||
|
descripcionEnfoque: string;
|
||||||
|
notasAdicionales: string;
|
||||||
|
archivosExistentesIds: Array<string>;
|
||||||
|
};
|
||||||
|
resumen: {
|
||||||
|
previewAsignatura?: AsignaturaPreview;
|
||||||
|
};
|
||||||
|
isLoading: boolean;
|
||||||
|
errorMessage: string | null;
|
||||||
|
};
|
||||||
205
src/features/planes/new/NuevoPlanModalContainer.tsx
Normal file
205
src/features/planes/new/NuevoPlanModalContainer.tsx
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
|
import * as Icons from 'lucide-react'
|
||||||
|
|
||||||
|
import { useNuevoPlanWizard } from './hooks/useNuevoPlanWizard'
|
||||||
|
|
||||||
|
import { PasoBasicosForm } from '@/components/planes/wizard/PasoBasicosForm'
|
||||||
|
import { PasoDetallesPanel } from '@/components/planes/wizard/PasoDetallesPanel'
|
||||||
|
import { PasoModoCardGroup } from '@/components/planes/wizard/PasoModoCardGroup'
|
||||||
|
import { PasoResumenCard } from '@/components/planes/wizard/PasoResumenCard'
|
||||||
|
import { WizardControls } from '@/components/planes/wizard/WizardControls'
|
||||||
|
import { WizardHeader } from '@/components/planes/wizard/WizardHeader'
|
||||||
|
import { defineStepper } from '@/components/stepper'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
|
||||||
|
// Mock de permisos/rol
|
||||||
|
const auth_get_current_user_role = () => 'JEFE_CARRERA' as const
|
||||||
|
|
||||||
|
const Wizard = defineStepper(
|
||||||
|
{
|
||||||
|
id: 'modo',
|
||||||
|
title: 'Método',
|
||||||
|
description: 'Selecciona cómo crearás el plan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'basicos',
|
||||||
|
title: 'Datos básicos',
|
||||||
|
description: 'Nombre, carrera, nivel y ciclos',
|
||||||
|
},
|
||||||
|
{ id: 'detalles', title: 'Detalles', description: 'IA, clonado o archivos' },
|
||||||
|
{ id: 'resumen', title: 'Resumen', description: 'Confirma y crea el plan' },
|
||||||
|
)
|
||||||
|
|
||||||
|
export default function NuevoPlanModalContainer() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const role = auth_get_current_user_role()
|
||||||
|
|
||||||
|
const {
|
||||||
|
wizard,
|
||||||
|
setWizard,
|
||||||
|
carrerasFiltradas,
|
||||||
|
canContinueDesdeModo,
|
||||||
|
canContinueDesdeBasicos,
|
||||||
|
canContinueDesdeDetalles,
|
||||||
|
generarPreviewIA,
|
||||||
|
} = useNuevoPlanWizard()
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
navigate({ to: '/planes', resetScroll: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
const crearPlan = async () => {
|
||||||
|
setWizard((w) => ({ ...w, isLoading: true, errorMessage: null }))
|
||||||
|
await new Promise((r) => setTimeout(r, 900))
|
||||||
|
const nuevoId = (() => {
|
||||||
|
if (wizard.modoCreacion === 'MANUAL') return 'plan_new_manual_001'
|
||||||
|
if (wizard.modoCreacion === 'IA') return 'plan_new_ai_001'
|
||||||
|
if (wizard.subModoClonado === 'INTERNO') return 'plan_new_clone_001'
|
||||||
|
return 'plan_new_import_001'
|
||||||
|
})()
|
||||||
|
navigate({ to: `/planes/${nuevoId}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={true} onOpenChange={(open) => !open && handleClose()}>
|
||||||
|
<DialogContent
|
||||||
|
className="flex h-[90vh] w-[calc(100%-2rem)] flex-col gap-0 overflow-hidden p-0 sm:max-w-4xl"
|
||||||
|
onInteractOutside={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{role !== 'JEFE_CARRERA' ? (
|
||||||
|
<>
|
||||||
|
<DialogHeader className="flex-none border-b p-6">
|
||||||
|
<DialogTitle>Nuevo plan de estudios</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex-1 p-6">
|
||||||
|
<Card className="border-destructive/40">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Icons.ShieldAlert className="text-destructive h-5 w-5" />
|
||||||
|
Sin permisos
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
No tienes permisos para crear planes de estudio.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex justify-end">
|
||||||
|
<button
|
||||||
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground rounded-md border px-3 py-2 text-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
Volver
|
||||||
|
</button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Wizard.Stepper.Provider
|
||||||
|
initialStep={Wizard.utils.getFirst().id}
|
||||||
|
className="flex h-full flex-col"
|
||||||
|
>
|
||||||
|
{({ methods }) => {
|
||||||
|
const currentIndex = Wizard.utils.getIndex(methods.current.id) + 1
|
||||||
|
const totalSteps = Wizard.steps.length
|
||||||
|
const nextStep = Wizard.steps[currentIndex]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<WizardHeader
|
||||||
|
currentIndex={currentIndex}
|
||||||
|
totalSteps={totalSteps}
|
||||||
|
currentTitle={methods.current.title}
|
||||||
|
currentDescription={methods.current.description}
|
||||||
|
nextTitle={nextStep?.title}
|
||||||
|
onClose={handleClose}
|
||||||
|
Wizard={Wizard}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto bg-gray-50/30 p-6">
|
||||||
|
<div className="mx-auto max-w-3xl">
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 0 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoModoCardGroup
|
||||||
|
wizard={wizard}
|
||||||
|
onChange={setWizard}
|
||||||
|
/>
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 1 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoBasicosForm
|
||||||
|
wizard={wizard}
|
||||||
|
onChange={setWizard}
|
||||||
|
carrerasFiltradas={carrerasFiltradas}
|
||||||
|
/>
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 2 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoDetallesPanel
|
||||||
|
wizard={wizard}
|
||||||
|
onChange={setWizard}
|
||||||
|
onGenerarIA={generarPreviewIA}
|
||||||
|
isLoading={wizard.isLoading}
|
||||||
|
/>
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 3 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoResumenCard wizard={wizard} />
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-none border-t bg-white p-6">
|
||||||
|
<Wizard.Stepper.Controls>
|
||||||
|
<WizardControls
|
||||||
|
errorMessage={wizard.errorMessage}
|
||||||
|
onPrev={() => methods.prev()}
|
||||||
|
onNext={() => methods.next()}
|
||||||
|
onCreate={crearPlan}
|
||||||
|
disablePrev={
|
||||||
|
Wizard.utils.getIndex(methods.current.id) === 0 ||
|
||||||
|
wizard.isLoading
|
||||||
|
}
|
||||||
|
disableNext={
|
||||||
|
wizard.isLoading ||
|
||||||
|
(Wizard.utils.getIndex(methods.current.id) === 0 &&
|
||||||
|
!canContinueDesdeModo) ||
|
||||||
|
(Wizard.utils.getIndex(methods.current.id) === 1 &&
|
||||||
|
!canContinueDesdeBasicos) ||
|
||||||
|
(Wizard.utils.getIndex(methods.current.id) === 2 &&
|
||||||
|
!canContinueDesdeDetalles)
|
||||||
|
}
|
||||||
|
disableCreate={wizard.isLoading}
|
||||||
|
isLastStep={
|
||||||
|
Wizard.utils.getIndex(methods.current.id) >=
|
||||||
|
Wizard.steps.length - 1
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Wizard.Stepper.Controls>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Wizard.Stepper.Provider>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
56
src/features/planes/new/catalogs.ts
Normal file
56
src/features/planes/new/catalogs.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import type { TipoCiclo } from "./types";
|
||||||
|
|
||||||
|
export const FACULTADES = [
|
||||||
|
{ id: "ing", nombre: "Facultad de Ingeniería" },
|
||||||
|
{
|
||||||
|
id: "med",
|
||||||
|
nombre: "Facultad de Medicina en medicina en medicina en medicina",
|
||||||
|
},
|
||||||
|
{ id: "neg", nombre: "Facultad de Negocios" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CARRERAS = [
|
||||||
|
{ id: "sis", nombre: "Ing. en Sistemas", facultadId: "ing" },
|
||||||
|
{ id: "ind", nombre: "Ing. Industrial", facultadId: "ing" },
|
||||||
|
{ id: "medico", nombre: "Médico Cirujano", facultadId: "med" },
|
||||||
|
{ id: "act", nombre: "Actuaría", facultadId: "neg" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const NIVELES = [
|
||||||
|
"Licenciatura",
|
||||||
|
"Especialidad",
|
||||||
|
"Maestría",
|
||||||
|
"Doctorado",
|
||||||
|
];
|
||||||
|
export const TIPOS_CICLO: Array<{ value: TipoCiclo; label: string }> = [
|
||||||
|
{ value: "SEMESTRE", label: "Semestre" },
|
||||||
|
{ value: "CUATRIMESTRE", label: "Cuatrimestre" },
|
||||||
|
{ value: "TRIMESTRE", label: "Trimestre" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PLANES_EXISTENTES = [
|
||||||
|
{
|
||||||
|
id: "plan-2021-sis",
|
||||||
|
nombre: "ISC 2021",
|
||||||
|
estado: "Aprobado",
|
||||||
|
anio: 2021,
|
||||||
|
facultadId: "ing",
|
||||||
|
carreraId: "sis",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "plan-2020-ind",
|
||||||
|
nombre: "I. Industrial 2020",
|
||||||
|
estado: "Aprobado",
|
||||||
|
anio: 2020,
|
||||||
|
facultadId: "ing",
|
||||||
|
carreraId: "ind",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "plan-2019-med",
|
||||||
|
nombre: "Medicina 2019",
|
||||||
|
estado: "Vigente",
|
||||||
|
anio: 2019,
|
||||||
|
facultadId: "med",
|
||||||
|
carreraId: "medico",
|
||||||
|
},
|
||||||
|
];
|
||||||
102
src/features/planes/new/hooks/useNuevoPlanWizard.ts
Normal file
102
src/features/planes/new/hooks/useNuevoPlanWizard.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { CARRERAS } from "../catalogs";
|
||||||
|
|
||||||
|
import type { NewPlanWizardState, PlanPreview } from "../types";
|
||||||
|
|
||||||
|
export function useNuevoPlanWizard() {
|
||||||
|
const [wizard, setWizard] = useState<NewPlanWizardState>({
|
||||||
|
step: 1,
|
||||||
|
modoCreacion: null,
|
||||||
|
datosBasicos: {
|
||||||
|
nombrePlan: "",
|
||||||
|
carreraId: "",
|
||||||
|
facultadId: "",
|
||||||
|
nivel: "",
|
||||||
|
tipoCiclo: "SEMESTRE",
|
||||||
|
numCiclos: 8,
|
||||||
|
},
|
||||||
|
clonInterno: { planOrigenId: null },
|
||||||
|
clonTradicional: {
|
||||||
|
archivoWordPlanId: null,
|
||||||
|
archivoMapaExcelId: null,
|
||||||
|
archivoAsignaturasExcelId: null,
|
||||||
|
},
|
||||||
|
iaConfig: {
|
||||||
|
descripcionEnfoque: "",
|
||||||
|
poblacionObjetivo: "",
|
||||||
|
notasAdicionales: "",
|
||||||
|
archivosReferencia: [],
|
||||||
|
},
|
||||||
|
resumen: {},
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const carrerasFiltradas = useMemo(() => {
|
||||||
|
const fac = wizard.datosBasicos.facultadId;
|
||||||
|
return fac ? CARRERAS.filter((c) => c.facultadId === fac) : CARRERAS;
|
||||||
|
}, [wizard.datosBasicos.facultadId]);
|
||||||
|
|
||||||
|
const canContinueDesdeModo = wizard.modoCreacion === "MANUAL" ||
|
||||||
|
wizard.modoCreacion === "IA" ||
|
||||||
|
(wizard.modoCreacion === "CLONADO" && !!wizard.subModoClonado);
|
||||||
|
|
||||||
|
const canContinueDesdeBasicos = !!wizard.datosBasicos.nombrePlan &&
|
||||||
|
!!wizard.datosBasicos.carreraId &&
|
||||||
|
!!wizard.datosBasicos.facultadId &&
|
||||||
|
!!wizard.datosBasicos.nivel &&
|
||||||
|
wizard.datosBasicos.numCiclos > 0;
|
||||||
|
|
||||||
|
const canContinueDesdeDetalles = (() => {
|
||||||
|
if (wizard.modoCreacion === "MANUAL") return true;
|
||||||
|
if (wizard.modoCreacion === "IA") {
|
||||||
|
return !!wizard.iaConfig?.descripcionEnfoque;
|
||||||
|
}
|
||||||
|
if (wizard.modoCreacion === "CLONADO") {
|
||||||
|
if (wizard.subModoClonado === "INTERNO") {
|
||||||
|
return !!wizard.clonInterno?.planOrigenId;
|
||||||
|
}
|
||||||
|
if (wizard.subModoClonado === "TRADICIONAL") {
|
||||||
|
const t = wizard.clonTradicional;
|
||||||
|
if (!t) return false;
|
||||||
|
const tieneWord = !!t.archivoWordPlanId;
|
||||||
|
const tieneAlMenosUnExcel = !!t.archivoMapaExcelId ||
|
||||||
|
!!t.archivoAsignaturasExcelId;
|
||||||
|
return tieneWord && tieneAlMenosUnExcel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const generarPreviewIA = async () => {
|
||||||
|
setWizard((w) => ({ ...w, isLoading: true, errorMessage: null }));
|
||||||
|
await new Promise((r) => setTimeout(r, 800));
|
||||||
|
const preview: PlanPreview = {
|
||||||
|
nombrePlan: wizard.datosBasicos.nombrePlan || "Plan sin nombre",
|
||||||
|
nivel: wizard.datosBasicos.nivel || "Licenciatura",
|
||||||
|
tipoCiclo: wizard.datosBasicos.tipoCiclo,
|
||||||
|
numCiclos: wizard.datosBasicos.numCiclos,
|
||||||
|
numAsignaturasAprox: wizard.datosBasicos.numCiclos * 6,
|
||||||
|
secciones: [
|
||||||
|
{ id: "obj", titulo: "Objetivos", resumen: "Borrador de objetivos…" },
|
||||||
|
{ id: "perfil", titulo: "Perfil de egreso", resumen: "Borrador…" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
setWizard((w) => ({
|
||||||
|
...w,
|
||||||
|
isLoading: false,
|
||||||
|
resumen: { previewPlan: preview },
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
wizard,
|
||||||
|
setWizard,
|
||||||
|
carrerasFiltradas,
|
||||||
|
canContinueDesdeModo,
|
||||||
|
canContinueDesdeBasicos,
|
||||||
|
canContinueDesdeDetalles,
|
||||||
|
generarPreviewIA,
|
||||||
|
};
|
||||||
|
}
|
||||||
41
src/features/planes/new/types.ts
Normal file
41
src/features/planes/new/types.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
export type TipoCiclo = "SEMESTRE" | "CUATRIMESTRE" | "TRIMESTRE";
|
||||||
|
export type ModoCreacion = "MANUAL" | "IA" | "CLONADO";
|
||||||
|
export type SubModoClonado = "INTERNO" | "TRADICIONAL";
|
||||||
|
|
||||||
|
export type PlanPreview = {
|
||||||
|
nombrePlan: string;
|
||||||
|
nivel: string;
|
||||||
|
tipoCiclo: TipoCiclo;
|
||||||
|
numCiclos: number;
|
||||||
|
numAsignaturasAprox?: number;
|
||||||
|
secciones?: Array<{ id: string; titulo: string; resumen: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NewPlanWizardState = {
|
||||||
|
step: 1 | 2 | 3 | 4;
|
||||||
|
modoCreacion: ModoCreacion | null;
|
||||||
|
subModoClonado?: SubModoClonado;
|
||||||
|
datosBasicos: {
|
||||||
|
nombrePlan: string;
|
||||||
|
carreraId: string;
|
||||||
|
facultadId: string;
|
||||||
|
nivel: string;
|
||||||
|
tipoCiclo: TipoCiclo;
|
||||||
|
numCiclos: number;
|
||||||
|
};
|
||||||
|
clonInterno?: { planOrigenId: string | null };
|
||||||
|
clonTradicional?: {
|
||||||
|
archivoWordPlanId: string | null;
|
||||||
|
archivoMapaExcelId: string | null;
|
||||||
|
archivoAsignaturasExcelId: string | null;
|
||||||
|
};
|
||||||
|
iaConfig?: {
|
||||||
|
descripcionEnfoque: string;
|
||||||
|
poblacionObjetivo: string;
|
||||||
|
notasAdicionales: string;
|
||||||
|
archivosReferencia: Array<string>;
|
||||||
|
};
|
||||||
|
resumen: { previewPlan?: PlanPreview };
|
||||||
|
isLoading: boolean;
|
||||||
|
errorMessage: string | null;
|
||||||
|
};
|
||||||
@@ -12,8 +12,13 @@ import { Route as rootRouteImport } from './routes/__root'
|
|||||||
import { Route as LoginRouteImport } from './routes/login'
|
import { Route as LoginRouteImport } from './routes/login'
|
||||||
import { Route as DashboardRouteImport } from './routes/dashboard'
|
import { Route as DashboardRouteImport } from './routes/dashboard'
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as IndexRouteImport } from './routes/index'
|
||||||
import { Route as PlanesIndexRouteImport } from './routes/planes/index'
|
|
||||||
import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query'
|
import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query'
|
||||||
|
import { Route as PlanesListaRouteRouteImport } from './routes/planes/_lista/route'
|
||||||
|
import { Route as PlanesPlanIdIndexRouteImport } from './routes/planes/$planId/index'
|
||||||
|
import { Route as PlanesListaNuevoRouteImport } from './routes/planes/_lista/nuevo'
|
||||||
|
import { Route as PlanesPlanIdAsignaturasListaRouteRouteImport } from './routes/planes/$planId/asignaturas/_lista/route'
|
||||||
|
import { Route as PlanesPlanIdAsignaturasAsignaturaIdRouteRouteImport } from './routes/planes/$planId/asignaturas/$asignaturaId/route'
|
||||||
|
import { Route as PlanesPlanIdAsignaturasListaNuevaRouteImport } from './routes/planes/$planId/asignaturas/_lista/nueva'
|
||||||
|
|
||||||
const LoginRoute = LoginRouteImport.update({
|
const LoginRoute = LoginRouteImport.update({
|
||||||
id: '/login',
|
id: '/login',
|
||||||
@@ -30,59 +35,130 @@ const IndexRoute = IndexRouteImport.update({
|
|||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
const PlanesIndexRoute = PlanesIndexRouteImport.update({
|
|
||||||
id: '/planes/',
|
|
||||||
path: '/planes/',
|
|
||||||
getParentRoute: () => rootRouteImport,
|
|
||||||
} as any)
|
|
||||||
const DemoTanstackQueryRoute = DemoTanstackQueryRouteImport.update({
|
const DemoTanstackQueryRoute = DemoTanstackQueryRouteImport.update({
|
||||||
id: '/demo/tanstack-query',
|
id: '/demo/tanstack-query',
|
||||||
path: '/demo/tanstack-query',
|
path: '/demo/tanstack-query',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any)
|
||||||
|
const PlanesListaRouteRoute = PlanesListaRouteRouteImport.update({
|
||||||
|
id: '/planes/_lista',
|
||||||
|
path: '/planes',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const PlanesPlanIdIndexRoute = PlanesPlanIdIndexRouteImport.update({
|
||||||
|
id: '/planes/$planId/',
|
||||||
|
path: '/planes/$planId/',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const PlanesListaNuevoRoute = PlanesListaNuevoRouteImport.update({
|
||||||
|
id: '/nuevo',
|
||||||
|
path: '/nuevo',
|
||||||
|
getParentRoute: () => PlanesListaRouteRoute,
|
||||||
|
} as any)
|
||||||
|
const PlanesPlanIdAsignaturasListaRouteRoute =
|
||||||
|
PlanesPlanIdAsignaturasListaRouteRouteImport.update({
|
||||||
|
id: '/planes/$planId/asignaturas/_lista',
|
||||||
|
path: '/planes/$planId/asignaturas',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const PlanesPlanIdAsignaturasAsignaturaIdRouteRoute =
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdRouteRouteImport.update({
|
||||||
|
id: '/planes/$planId/asignaturas/$asignaturaId',
|
||||||
|
path: '/planes/$planId/asignaturas/$asignaturaId',
|
||||||
|
getParentRoute: () => rootRouteImport,
|
||||||
|
} as any)
|
||||||
|
const PlanesPlanIdAsignaturasListaNuevaRoute =
|
||||||
|
PlanesPlanIdAsignaturasListaNuevaRouteImport.update({
|
||||||
|
id: '/nueva',
|
||||||
|
path: '/nueva',
|
||||||
|
getParentRoute: () => PlanesPlanIdAsignaturasListaRouteRoute,
|
||||||
|
} as any)
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/dashboard': typeof DashboardRoute
|
'/dashboard': typeof DashboardRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
|
'/planes': typeof PlanesListaRouteRouteWithChildren
|
||||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||||
'/planes': typeof PlanesIndexRoute
|
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
||||||
|
'/planes/$planId': typeof PlanesPlanIdIndexRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
||||||
|
'/planes/$planId/asignaturas': typeof PlanesPlanIdAsignaturasListaRouteRouteWithChildren
|
||||||
|
'/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdAsignaturasListaNuevaRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/dashboard': typeof DashboardRoute
|
'/dashboard': typeof DashboardRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
|
'/planes': typeof PlanesListaRouteRouteWithChildren
|
||||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||||
'/planes': typeof PlanesIndexRoute
|
'/planes/nuevo': typeof PlanesListaNuevoRoute
|
||||||
|
'/planes/$planId': typeof PlanesPlanIdIndexRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
||||||
|
'/planes/$planId/asignaturas': typeof PlanesPlanIdAsignaturasListaRouteRouteWithChildren
|
||||||
|
'/planes/$planId/asignaturas/nueva': typeof PlanesPlanIdAsignaturasListaNuevaRoute
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/dashboard': typeof DashboardRoute
|
'/dashboard': typeof DashboardRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
|
'/planes/_lista': typeof PlanesListaRouteRouteWithChildren
|
||||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||||
'/planes/': typeof PlanesIndexRoute
|
'/planes/_lista/nuevo': typeof PlanesListaNuevoRoute
|
||||||
|
'/planes/$planId/': typeof PlanesPlanIdIndexRoute
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId': typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
||||||
|
'/planes/$planId/asignaturas/_lista': typeof PlanesPlanIdAsignaturasListaRouteRouteWithChildren
|
||||||
|
'/planes/$planId/asignaturas/_lista/nueva': typeof PlanesPlanIdAsignaturasListaNuevaRoute
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths: '/' | '/dashboard' | '/login' | '/demo/tanstack-query' | '/planes'
|
fullPaths:
|
||||||
|
| '/'
|
||||||
|
| '/dashboard'
|
||||||
|
| '/login'
|
||||||
|
| '/planes'
|
||||||
|
| '/demo/tanstack-query'
|
||||||
|
| '/planes/nuevo'
|
||||||
|
| '/planes/$planId'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
|
| '/planes/$planId/asignaturas'
|
||||||
|
| '/planes/$planId/asignaturas/nueva'
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to: '/' | '/dashboard' | '/login' | '/demo/tanstack-query' | '/planes'
|
to:
|
||||||
|
| '/'
|
||||||
|
| '/dashboard'
|
||||||
|
| '/login'
|
||||||
|
| '/planes'
|
||||||
|
| '/demo/tanstack-query'
|
||||||
|
| '/planes/nuevo'
|
||||||
|
| '/planes/$planId'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
|
| '/planes/$planId/asignaturas'
|
||||||
|
| '/planes/$planId/asignaturas/nueva'
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
| '/dashboard'
|
| '/dashboard'
|
||||||
| '/login'
|
| '/login'
|
||||||
|
| '/planes/_lista'
|
||||||
| '/demo/tanstack-query'
|
| '/demo/tanstack-query'
|
||||||
| '/planes/'
|
| '/planes/_lista/nuevo'
|
||||||
|
| '/planes/$planId/'
|
||||||
|
| '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
|
| '/planes/$planId/asignaturas/_lista'
|
||||||
|
| '/planes/$planId/asignaturas/_lista/nueva'
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute
|
||||||
DashboardRoute: typeof DashboardRoute
|
DashboardRoute: typeof DashboardRoute
|
||||||
LoginRoute: typeof LoginRoute
|
LoginRoute: typeof LoginRoute
|
||||||
|
PlanesListaRouteRoute: typeof PlanesListaRouteRouteWithChildren
|
||||||
DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
|
DemoTanstackQueryRoute: typeof DemoTanstackQueryRoute
|
||||||
PlanesIndexRoute: typeof PlanesIndexRoute
|
PlanesPlanIdIndexRoute: typeof PlanesPlanIdIndexRoute
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdRouteRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRoute
|
||||||
|
PlanesPlanIdAsignaturasListaRouteRoute: typeof PlanesPlanIdAsignaturasListaRouteRouteWithChildren
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
@@ -108,13 +184,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof IndexRouteImport
|
preLoaderRoute: typeof IndexRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
'/planes/': {
|
|
||||||
id: '/planes/'
|
|
||||||
path: '/planes'
|
|
||||||
fullPath: '/planes'
|
|
||||||
preLoaderRoute: typeof PlanesIndexRouteImport
|
|
||||||
parentRoute: typeof rootRouteImport
|
|
||||||
}
|
|
||||||
'/demo/tanstack-query': {
|
'/demo/tanstack-query': {
|
||||||
id: '/demo/tanstack-query'
|
id: '/demo/tanstack-query'
|
||||||
path: '/demo/tanstack-query'
|
path: '/demo/tanstack-query'
|
||||||
@@ -122,15 +191,88 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof DemoTanstackQueryRouteImport
|
preLoaderRoute: typeof DemoTanstackQueryRouteImport
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport
|
||||||
}
|
}
|
||||||
|
'/planes/_lista': {
|
||||||
|
id: '/planes/_lista'
|
||||||
|
path: '/planes'
|
||||||
|
fullPath: '/planes'
|
||||||
|
preLoaderRoute: typeof PlanesListaRouteRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/planes/$planId/': {
|
||||||
|
id: '/planes/$planId/'
|
||||||
|
path: '/planes/$planId'
|
||||||
|
fullPath: '/planes/$planId'
|
||||||
|
preLoaderRoute: typeof PlanesPlanIdIndexRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/planes/_lista/nuevo': {
|
||||||
|
id: '/planes/_lista/nuevo'
|
||||||
|
path: '/nuevo'
|
||||||
|
fullPath: '/planes/nuevo'
|
||||||
|
preLoaderRoute: typeof PlanesListaNuevoRouteImport
|
||||||
|
parentRoute: typeof PlanesListaRouteRoute
|
||||||
|
}
|
||||||
|
'/planes/$planId/asignaturas/_lista': {
|
||||||
|
id: '/planes/$planId/asignaturas/_lista'
|
||||||
|
path: '/planes/$planId/asignaturas'
|
||||||
|
fullPath: '/planes/$planId/asignaturas'
|
||||||
|
preLoaderRoute: typeof PlanesPlanIdAsignaturasListaRouteRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/planes/$planId/asignaturas/$asignaturaId': {
|
||||||
|
id: '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
|
path: '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
|
fullPath: '/planes/$planId/asignaturas/$asignaturaId'
|
||||||
|
preLoaderRoute: typeof PlanesPlanIdAsignaturasAsignaturaIdRouteRouteImport
|
||||||
|
parentRoute: typeof rootRouteImport
|
||||||
|
}
|
||||||
|
'/planes/$planId/asignaturas/_lista/nueva': {
|
||||||
|
id: '/planes/$planId/asignaturas/_lista/nueva'
|
||||||
|
path: '/nueva'
|
||||||
|
fullPath: '/planes/$planId/asignaturas/nueva'
|
||||||
|
preLoaderRoute: typeof PlanesPlanIdAsignaturasListaNuevaRouteImport
|
||||||
|
parentRoute: typeof PlanesPlanIdAsignaturasListaRouteRoute
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PlanesListaRouteRouteChildren {
|
||||||
|
PlanesListaNuevoRoute: typeof PlanesListaNuevoRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlanesListaRouteRouteChildren: PlanesListaRouteRouteChildren = {
|
||||||
|
PlanesListaNuevoRoute: PlanesListaNuevoRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlanesListaRouteRouteWithChildren =
|
||||||
|
PlanesListaRouteRoute._addFileChildren(PlanesListaRouteRouteChildren)
|
||||||
|
|
||||||
|
interface PlanesPlanIdAsignaturasListaRouteRouteChildren {
|
||||||
|
PlanesPlanIdAsignaturasListaNuevaRoute: typeof PlanesPlanIdAsignaturasListaNuevaRoute
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlanesPlanIdAsignaturasListaRouteRouteChildren: PlanesPlanIdAsignaturasListaRouteRouteChildren =
|
||||||
|
{
|
||||||
|
PlanesPlanIdAsignaturasListaNuevaRoute:
|
||||||
|
PlanesPlanIdAsignaturasListaNuevaRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
const PlanesPlanIdAsignaturasListaRouteRouteWithChildren =
|
||||||
|
PlanesPlanIdAsignaturasListaRouteRoute._addFileChildren(
|
||||||
|
PlanesPlanIdAsignaturasListaRouteRouteChildren,
|
||||||
|
)
|
||||||
|
|
||||||
const rootRouteChildren: RootRouteChildren = {
|
const rootRouteChildren: RootRouteChildren = {
|
||||||
IndexRoute: IndexRoute,
|
IndexRoute: IndexRoute,
|
||||||
DashboardRoute: DashboardRoute,
|
DashboardRoute: DashboardRoute,
|
||||||
LoginRoute: LoginRoute,
|
LoginRoute: LoginRoute,
|
||||||
|
PlanesListaRouteRoute: PlanesListaRouteRouteWithChildren,
|
||||||
DemoTanstackQueryRoute: DemoTanstackQueryRoute,
|
DemoTanstackQueryRoute: DemoTanstackQueryRoute,
|
||||||
PlanesIndexRoute: PlanesIndexRoute,
|
PlanesPlanIdIndexRoute: PlanesPlanIdIndexRoute,
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdRouteRoute:
|
||||||
|
PlanesPlanIdAsignaturasAsignaturaIdRouteRoute,
|
||||||
|
PlanesPlanIdAsignaturasListaRouteRoute:
|
||||||
|
PlanesPlanIdAsignaturasListaRouteRouteWithChildren,
|
||||||
}
|
}
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._addFileChildren(rootRouteChildren)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
||||||
import DashboardHeader from '@/components/dashboard/DashboardHeader'
|
import DashboardHeader from '@/components/dashboard/DashboardHeader'
|
||||||
import PlanEstudiosCard from '@/components/plan_estudios/PlanEstudiosCard'
|
import PlanEstudiosCard from '@/components/planes/PlanEstudiosCard'
|
||||||
|
|
||||||
export const Route = createFileRoute('/dashboard')({
|
export const Route = createFileRoute('/dashboard')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
import { LoginCard } from '@/components/auth/LoginCard'
|
import { LoginCard } from '@/components/auth/LoginCard'
|
||||||
|
|
||||||
export const Route = createFileRoute('/login')({
|
export const Route = createFileRoute('/login')({
|
||||||
@@ -7,7 +8,7 @@ export const Route = createFileRoute('/login')({
|
|||||||
|
|
||||||
function LoginPage() {
|
function LoginPage() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-[#7b0f1d] via-[#6b0d1a] to-[#3a050a] flex items-center justify-center">
|
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-[#7b0f1d] via-[#6b0d1a] to-[#3a050a]">
|
||||||
<LoginCard />
|
<LoginCard />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/planes/$planId/asignaturas/$asignaturaId')({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return <div>Hello "/asignaturas/$asignaturaId"!</div>
|
||||||
|
}
|
||||||
14
src/routes/planes/$planId/asignaturas/_lista/nueva.tsx
Normal file
14
src/routes/planes/$planId/asignaturas/_lista/nueva.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import { NuevaAsignaturaModalContainer } from '@/features/asignaturas/new/NuevaAsignaturaModalContainer'
|
||||||
|
|
||||||
|
export const Route = createFileRoute(
|
||||||
|
'/planes/$planId/asignaturas/_lista/nueva',
|
||||||
|
)({
|
||||||
|
component: NuevaAsignaturaModal,
|
||||||
|
})
|
||||||
|
|
||||||
|
function NuevaAsignaturaModal() {
|
||||||
|
const { planId } = Route.useParams()
|
||||||
|
return <NuevaAsignaturaModalContainer planId={planId} />
|
||||||
|
}
|
||||||
16
src/routes/planes/$planId/asignaturas/_lista/route.tsx
Normal file
16
src/routes/planes/$planId/asignaturas/_lista/route.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/planes/$planId/asignaturas/_lista')({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return (
|
||||||
|
<main className="bg-background min-h-screen w-full">
|
||||||
|
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4 px-4 py-6 md:px-6 lg:px-8">
|
||||||
|
<h1 className="text-foreground text-2xl font-semibold">Asignaturas</h1>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
10
src/routes/planes/$planId/index.tsx
Normal file
10
src/routes/planes/$planId/index.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/planes/$planId/')({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const { planId } = Route.useParams()
|
||||||
|
return <div>Hello "/planes/{planId}"!</div>
|
||||||
|
}
|
||||||
7
src/routes/planes/_lista/nuevo.tsx
Normal file
7
src/routes/planes/_lista/nuevo.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import NuevoPlanModalContainer from '@/features/planes/new/NuevoPlanModalContainer'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/planes/_lista/nuevo')({
|
||||||
|
component: NuevoPlanModalContainer,
|
||||||
|
})
|
||||||
324
src/routes/planes/_lista/route.tsx
Normal file
324
src/routes/planes/_lista/route.tsx
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
import { createFileRoute, Outlet, useNavigate } from '@tanstack/react-router'
|
||||||
|
import * as Icons from 'lucide-react'
|
||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
|
||||||
|
import type { Option } from '@/components/planes/Filtro'
|
||||||
|
|
||||||
|
import BarraBusqueda from '@/components/planes/BarraBusqueda'
|
||||||
|
import Filtro from '@/components/planes/Filtro'
|
||||||
|
import PlanEstudiosCard from '@/components/planes/PlanEstudiosCard'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/planes/_lista')({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
type Facultad = { id: string; nombre: string; color: string }
|
||||||
|
type Carrera = { id: string; nombre: string; facultadId: string }
|
||||||
|
type Plan = {
|
||||||
|
id: string
|
||||||
|
icon: string
|
||||||
|
nombrePrograma: string
|
||||||
|
nivel: string
|
||||||
|
ciclos: string
|
||||||
|
facultadId: string
|
||||||
|
carreraId: string
|
||||||
|
estado:
|
||||||
|
| 'Aprobado'
|
||||||
|
| 'Pendiente'
|
||||||
|
| 'En proceso'
|
||||||
|
| 'Revisión expertos'
|
||||||
|
| 'Actualización'
|
||||||
|
claseColorEstado: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulación: datos provenientes de Supabase (hardcode)
|
||||||
|
const facultades: Array<Facultad> = [
|
||||||
|
{ id: 'ing', nombre: 'Facultad de Ingeniería', color: '#2563eb' },
|
||||||
|
{ id: 'med', nombre: 'Facultad de Medicina', color: '#dc2626' },
|
||||||
|
{ id: 'neg', nombre: 'Facultad de Negocios', color: '#059669' },
|
||||||
|
{
|
||||||
|
id: 'arq',
|
||||||
|
nombre: 'Facultad Mexicana de Arquitectura, Diseño y Comunicación',
|
||||||
|
color: '#ea580c',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sal',
|
||||||
|
nombre: 'Escuela de Altos Estudios en Salud',
|
||||||
|
color: '#0891b2',
|
||||||
|
},
|
||||||
|
{ id: 'der', nombre: 'Facultad de Derecho', color: '#7c3aed' },
|
||||||
|
{ id: 'qui', nombre: 'Facultad de Ciencias Químicas', color: '#65a30d' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const carreras: Array<Carrera> = [
|
||||||
|
{
|
||||||
|
id: 'sis',
|
||||||
|
nombre: 'Ingeniería en Sistemas Computacionales',
|
||||||
|
facultadId: 'ing',
|
||||||
|
},
|
||||||
|
{ id: 'medico', nombre: 'Médico Cirujano', facultadId: 'med' },
|
||||||
|
{ id: 'act', nombre: 'Licenciatura en Actuaría', facultadId: 'neg' },
|
||||||
|
{ id: 'arq', nombre: 'Licenciatura en Arquitectura', facultadId: 'arq' },
|
||||||
|
{ id: 'fisio', nombre: 'Licenciatura en Fisioterapia', facultadId: 'sal' },
|
||||||
|
{ id: 'der', nombre: 'Licenciatura en Derecho', facultadId: 'der' },
|
||||||
|
{ id: 'qfb', nombre: 'Químico Farmacéutico Biólogo', facultadId: 'qui' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const estados: Array<Option> = [
|
||||||
|
{ value: 'todos', label: 'Todos los estados' },
|
||||||
|
{ value: 'Aprobado', label: 'Aprobado' },
|
||||||
|
{ value: 'Pendiente', label: 'Pendiente' },
|
||||||
|
{ value: 'En proceso', label: 'En proceso' },
|
||||||
|
{ value: 'Revisión expertos', label: 'Revisión expertos' },
|
||||||
|
{ value: 'Actualización', label: 'Actualización' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const planes: Array<Plan> = [
|
||||||
|
{
|
||||||
|
id: 'p1',
|
||||||
|
icon: 'Laptop',
|
||||||
|
nombrePrograma: 'Ingeniería en Sistemas Computacionales',
|
||||||
|
nivel: 'Licenciatura',
|
||||||
|
ciclos: '8 semestres',
|
||||||
|
facultadId: 'ing',
|
||||||
|
carreraId: 'sis',
|
||||||
|
estado: 'Revisión expertos',
|
||||||
|
claseColorEstado: 'bg-amber-600',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p2',
|
||||||
|
icon: 'Stethoscope',
|
||||||
|
nombrePrograma: 'Médico Cirujano',
|
||||||
|
nivel: 'Licenciatura',
|
||||||
|
ciclos: '10 semestres',
|
||||||
|
facultadId: 'med',
|
||||||
|
carreraId: 'medico',
|
||||||
|
estado: 'Aprobado',
|
||||||
|
claseColorEstado: 'bg-emerald-600',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p3',
|
||||||
|
icon: 'Calculator',
|
||||||
|
nombrePrograma: 'Licenciatura en Actuaría',
|
||||||
|
nivel: 'Licenciatura',
|
||||||
|
ciclos: '9 semestres',
|
||||||
|
facultadId: 'neg',
|
||||||
|
carreraId: 'act',
|
||||||
|
estado: 'Aprobado',
|
||||||
|
claseColorEstado: 'bg-emerald-600',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p4',
|
||||||
|
icon: 'PencilRuler',
|
||||||
|
nombrePrograma: 'Licenciatura en Arquitectura',
|
||||||
|
nivel: 'Licenciatura',
|
||||||
|
ciclos: '10 semestres',
|
||||||
|
facultadId: 'arq',
|
||||||
|
carreraId: 'arq',
|
||||||
|
estado: 'En proceso',
|
||||||
|
claseColorEstado: 'bg-orange-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p5',
|
||||||
|
icon: 'Activity',
|
||||||
|
nombrePrograma: 'Licenciatura en Fisioterapia',
|
||||||
|
nivel: 'Licenciatura',
|
||||||
|
ciclos: '8 semestres',
|
||||||
|
facultadId: 'sal',
|
||||||
|
carreraId: 'fisio',
|
||||||
|
estado: 'Revisión expertos',
|
||||||
|
claseColorEstado: 'bg-amber-600',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p6',
|
||||||
|
icon: 'Scale',
|
||||||
|
nombrePrograma: 'Licenciatura en Derecho',
|
||||||
|
nivel: 'Licenciatura',
|
||||||
|
ciclos: '10 semestres',
|
||||||
|
facultadId: 'der',
|
||||||
|
carreraId: 'der',
|
||||||
|
estado: 'Pendiente',
|
||||||
|
claseColorEstado: 'bg-yellow-500',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p7',
|
||||||
|
icon: 'FlaskConical',
|
||||||
|
nombrePrograma: 'Químico Farmacéutico Biólogo',
|
||||||
|
nivel: 'Licenciatura',
|
||||||
|
ciclos: '9 semestres',
|
||||||
|
facultadId: 'qui',
|
||||||
|
carreraId: 'qfb',
|
||||||
|
estado: 'Actualización',
|
||||||
|
claseColorEstado: 'bg-lime-600',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// Estado de filtros
|
||||||
|
const [search, setSearch] = useState('')
|
||||||
|
const [facultadSel, setFacultadSel] = useState<string>('todas')
|
||||||
|
const [carreraSel, setCarreraSel] = useState<string>('todas')
|
||||||
|
const [estadoSel, setEstadoSel] = useState<string>('todos')
|
||||||
|
|
||||||
|
// Opciones para filtros
|
||||||
|
const facultadesOptions: Array<Option> = useMemo(
|
||||||
|
() => [
|
||||||
|
{ value: 'todas', label: 'Todas las facultades' },
|
||||||
|
...facultades.map((f) => ({ value: f.id, label: f.nombre })),
|
||||||
|
],
|
||||||
|
[facultades],
|
||||||
|
)
|
||||||
|
|
||||||
|
const carrerasOptions: Array<Option> = useMemo(() => {
|
||||||
|
const list =
|
||||||
|
facultadSel === 'todas'
|
||||||
|
? carreras
|
||||||
|
: carreras.filter((c) => c.facultadId === facultadSel)
|
||||||
|
return [
|
||||||
|
{ value: 'todas', label: 'Todas las carreras' },
|
||||||
|
...list.map((c) => ({ value: c.id, label: c.nombre })),
|
||||||
|
]
|
||||||
|
}, [carreras, facultadSel])
|
||||||
|
|
||||||
|
// Filtrado de planes
|
||||||
|
const filteredPlans = useMemo(() => {
|
||||||
|
const term = search.trim().toLowerCase()
|
||||||
|
return planes.filter((p) => {
|
||||||
|
const matchName = term
|
||||||
|
? p.nombrePrograma.toLowerCase().includes(term)
|
||||||
|
: true
|
||||||
|
const matchFac =
|
||||||
|
facultadSel === 'todas' ? true : p.facultadId === facultadSel
|
||||||
|
const matchCar =
|
||||||
|
carreraSel === 'todas' ? true : p.carreraId === carreraSel
|
||||||
|
const matchEst = estadoSel === 'todos' ? true : p.estado === estadoSel
|
||||||
|
return matchName && matchFac && matchCar && matchEst
|
||||||
|
})
|
||||||
|
}, [planes, search, facultadSel, carreraSel, estadoSel])
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
setSearch('')
|
||||||
|
setFacultadSel('todas')
|
||||||
|
setCarreraSel('todas')
|
||||||
|
setEstadoSel('todos')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="bg-background min-h-screen w-full">
|
||||||
|
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4 px-4 py-6 md:px-6 lg:px-8">
|
||||||
|
<div className="flex flex-col gap-4 lg:col-span-3">
|
||||||
|
<div className="flex flex-col items-stretch justify-between gap-4 sm:flex-row sm:items-center">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="bg-primary/10 text-primary flex h-10 w-10 items-center justify-center rounded-xl">
|
||||||
|
<Icons.BookOpenText className="h-5 w-5" strokeWidth={2} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 className="font-display text-foreground text-2xl font-bold">
|
||||||
|
Planes de Estudio
|
||||||
|
</h1>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Gestiona los planes curriculares de tu institución
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={
|
||||||
|
'ring-offset-background focus-visible:ring-ring bg-primary text-primary-foreground hover:bg-primary/90 inline-flex h-11 items-center justify-center gap-2 rounded-md px-8 text-sm font-medium whitespace-nowrap shadow-md transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:h-4 [&_svg]:w-4 [&_svg]:shrink-0'
|
||||||
|
}
|
||||||
|
aria-label="Nuevo plan de estudios"
|
||||||
|
title="Nuevo plan de estudios"
|
||||||
|
onClick={() => {
|
||||||
|
navigate({ to: '/planes/nuevo' })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icons.Plus className="" />
|
||||||
|
Nuevo plan de estudios
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-stretch gap-2 lg:flex-row lg:items-center">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<BarraBusqueda
|
||||||
|
value={search}
|
||||||
|
onChange={setSearch}
|
||||||
|
placeholder="Buscar por programa…"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-stretch justify-between gap-2 lg:flex-row lg:items-center">
|
||||||
|
<div className="w-full lg:w-44">
|
||||||
|
<Filtro
|
||||||
|
options={facultadesOptions}
|
||||||
|
value={facultadSel}
|
||||||
|
onChange={(v) => {
|
||||||
|
setFacultadSel(v)
|
||||||
|
// Reset carrera si ya no pertenece
|
||||||
|
setCarreraSel('todas')
|
||||||
|
}}
|
||||||
|
placeholder="Facultad"
|
||||||
|
ariaLabel="Filtro por facultad"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full lg:w-44">
|
||||||
|
<Filtro
|
||||||
|
options={carrerasOptions}
|
||||||
|
value={carreraSel}
|
||||||
|
onChange={setCarreraSel}
|
||||||
|
placeholder="Carrera"
|
||||||
|
ariaLabel="Filtro por carrera"
|
||||||
|
disabled={facultadSel === 'todas'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="w-full lg:w-44">
|
||||||
|
<Filtro
|
||||||
|
options={estados}
|
||||||
|
value={estadoSel}
|
||||||
|
onChange={setEstadoSel}
|
||||||
|
placeholder="Estado"
|
||||||
|
ariaLabel="Filtro por estado"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={resetFilters}
|
||||||
|
className={
|
||||||
|
'ring-offset-background focus-visible:ring-ring bg-secondary text-secondary-foreground hover:bg-secondary/90 inline-flex h-9 items-center justify-center gap-2 rounded-md px-4 text-sm font-medium whitespace-nowrap shadow-md transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50'
|
||||||
|
}
|
||||||
|
title="Reiniciar filtros"
|
||||||
|
aria-label="Reiniciar filtros"
|
||||||
|
>
|
||||||
|
<Icons.X className="h-4 w-4" />
|
||||||
|
Limpiar
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
|
{filteredPlans.map((p) => {
|
||||||
|
const fac = facultades.find((f) => f.id === p.facultadId)!
|
||||||
|
const IconComp = (Icons as any)[p.icon] ?? Icons.BookOpenText
|
||||||
|
return (
|
||||||
|
<PlanEstudiosCard
|
||||||
|
key={p.id}
|
||||||
|
Icono={IconComp}
|
||||||
|
nombrePrograma={p.nombrePrograma}
|
||||||
|
nivel={p.nivel}
|
||||||
|
ciclos={p.ciclos}
|
||||||
|
facultad={fac.nombre}
|
||||||
|
estado={p.estado}
|
||||||
|
claseColorEstado={p.claseColorEstado}
|
||||||
|
colorFacultad={fac.color}
|
||||||
|
onClick={() => console.log('Ver', p.nombrePrograma)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
|
||||||
import { AppLayout } from '@/components/layout/AppLayout'
|
|
||||||
import { PlanGrid } from '@/components/plans/PlanGrid'
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/planes/')({
|
|
||||||
component: PlanesPage,
|
|
||||||
})
|
|
||||||
|
|
||||||
function PlanesPage() {
|
|
||||||
return (
|
|
||||||
<AppLayout>
|
|
||||||
<PlanGrid />
|
|
||||||
</AppLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user