finalizada sección de Referencias para la IA

This commit is contained in:
2026-01-06 17:02:55 -06:00
parent ef177a3f92
commit 69119aeaa6
29 changed files with 1337 additions and 776 deletions

View File

@@ -1,17 +1,18 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "acad-ia-2",
"dependencies": {
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@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-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@stepperize/react": "^5.1.9",
"@tailwindcss/vite": "^4.0.6",
@@ -24,10 +25,11 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"lucide-react": "^0.561.0",
"lucide-react": "^0.562.0",
"motion": "^12.24.7",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwind-merge": "^3.0.2",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.0.6",
"tw-animate-css": "^1.3.6",
},
@@ -254,6 +256,8 @@
"@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.11", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q=="],
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="],
"@radix-ui/react-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=="],
@@ -284,12 +288,16 @@
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
"@radix-ui/react-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-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-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
"@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=="],
@@ -804,6 +812,8 @@
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
"framer-motion": ["framer-motion@12.24.7", "", { "dependencies": { "motion-dom": "^12.24.3", "motion-utils": "^12.23.28", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-EolFLm7NdEMhWO/VTMZ0LlR4fLHGDiJItTx3i8dlyQooOOBoYAaysK4paGD4PrwqnoDdeDOS+TxnSBIAnNHs3w=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
@@ -994,7 +1004,7 @@
"lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="],
"lucide-react": ["lucide-react@0.561.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A=="],
"lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="],
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
@@ -1008,6 +1018,12 @@
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"motion": ["motion@12.24.7", "", { "dependencies": { "framer-motion": "^12.24.7", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-0jOoqFlQ7JBvAcRhRv28pwUgZ1xw9WS4+tCU6aqYjxgiNVZCVi34ED2cihW3EgjIIWPBoZJis5og1mx/LmQWVQ=="],
"motion-dom": ["motion-dom@12.24.3", "", { "dependencies": { "motion-utils": "^12.23.28" } }, "sha512-ZjMZCwhTglim0LM64kC1iFdm4o+2P9IKk3rl/Nb4RKsb5p4O9HJ1C2LWZXOFdsRtp6twpqWRXaFKOduF30ntow=="],
"motion-utils": ["motion-utils@12.23.28", "", {}, "sha512-0W6cWd5Okoyf8jmessVK3spOmbyE0yTdNKujHctHH9XdAE4QDuZ1/LjSXC68rrhsJU+TkzXURC5OdSWh9ibOwQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
@@ -1310,6 +1326,10 @@
"@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-checkbox/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-checkbox/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-collection/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-collection/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
@@ -1338,12 +1358,20 @@
"@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-roving-focus/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-roving-focus/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-select/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-select/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-tabs/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-tabs/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-tooltip/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
@@ -1414,6 +1442,8 @@
"@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-checkbox/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-focus-scope/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
@@ -1422,6 +1452,10 @@
"@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@tanstack/devtools/@tanstack/devtools-client/@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.3.5", "", {}, "sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw=="],

View File

@@ -17,5 +17,11 @@
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
"iconLibrary": "lucide",
"registries": {
"@shadcn-studio": "https://shadcnstudio.com/r/{name}.json",
"@ss-components": "https://shadcnstudio.com/r/components/{name}.json",
"@ss-blocks": "https://shadcnstudio.com/r/blocks/{name}.json",
"@ss-themes": "https://shadcnstudio.com/r/themes/{name}.json"
}
}

View File

@@ -18,12 +18,14 @@
},
"dependencies": {
"@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-checkbox": "^1.3.3",
"@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-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@stepperize/react": "^5.1.9",
"@tailwindcss/vite": "^4.0.6",
@@ -36,10 +38,11 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"lucide-react": "^0.561.0",
"lucide-react": "^0.562.0",
"motion": "^12.24.7",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwind-merge": "^3.0.2",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.0.6",
"tw-animate-css": "^1.3.6"
},

View File

@@ -1,7 +1,7 @@
import type {
NewSubjectWizardState,
TipoAsignatura,
} from '@/features/asignaturas/new/types'
} from '@/features/asignaturas/nueva/types'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
@@ -15,7 +15,7 @@ import {
import {
ESTRUCTURAS_SEP,
TIPOS_MATERIA,
} from '@/features/asignaturas/new/catalogs'
} from '@/features/asignaturas/nueva/catalogs'
export function PasoBasicosForm({
wizard,

View File

@@ -1,6 +1,6 @@
import * as Icons from 'lucide-react'
import type { NewSubjectWizardState } from '@/features/asignaturas/new/types'
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
import { Button } from '@/components/ui/button'
import {
@@ -25,7 +25,7 @@ import {
FACULTADES,
MATERIAS_MOCK,
PLANES_MOCK,
} from '@/features/asignaturas/new/catalogs'
} from '@/features/asignaturas/nueva/catalogs'
export function PasoConfiguracionPanel({
wizard,

View File

@@ -4,7 +4,7 @@ import type {
ModoCreacion,
NewSubjectWizardState,
SubModoClonado,
} from '@/features/asignaturas/new/types'
} from '@/features/asignaturas/nueva/types'
import {
Card,

View File

@@ -1,6 +1,6 @@
import * as Icons from 'lucide-react'
import type { NewSubjectWizardState } from '@/features/asignaturas/new/types'
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
import {
Card,
@@ -9,7 +9,7 @@ import {
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { ESTRUCTURAS_SEP } from '@/features/asignaturas/new/catalogs'
import { ESTRUCTURAS_SEP } from '@/features/asignaturas/nueva/catalogs'
export function PasoResumenCard({ wizard }: { wizard: NewSubjectWizardState }) {
return (

View File

@@ -1,4 +1,4 @@
import type { NewSubjectWizardState } from '@/features/asignaturas/new/types'
import type { NewSubjectWizardState } from '@/features/asignaturas/nueva/types'
import { Button } from '@/components/ui/button'

View File

@@ -1,5 +1,5 @@
import type { CARRERAS } from '@/features/planes/new/catalogs'
import type { NewPlanWizardState } from '@/features/planes/new/types'
import type { CARRERAS } from '@/features/planes/nuevo/catalogs'
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
@@ -14,7 +14,7 @@ import {
FACULTADES,
NIVELES,
TIPOS_CICLO,
} from '@/features/planes/new/catalogs'
} from '@/features/planes/nuevo/catalogs'
export function PasoBasicosForm({
wizard,
@@ -146,7 +146,7 @@ export function PasoBasicosForm({
id="tipoCiclo"
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
>
<SelectValue />
<SelectValue placeholder="Selecciona tipo de ciclo…" />
</SelectTrigger>
<SelectContent>
{TIPOS_CICLO.map((t) => (
@@ -174,6 +174,7 @@ export function PasoBasicosForm({
},
}))
}
placeholder="1"
/>
</div>
</div>

View File

@@ -1,4 +1,6 @@
import type { NewPlanWizardState } from '@/features/planes/new/types'
import ReferenciasParaIA from './PasoDetallesPanel/ReferenciasParaIA'
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
import { Button } from '@/components/ui/button'
import {
@@ -14,7 +16,7 @@ import {
CARRERAS,
FACULTADES,
PLANES_EXISTENTES,
} from '@/features/planes/new/catalogs'
} from '@/features/planes/nuevo/catalogs'
export function PasoDetallesPanel({
wizard,
@@ -42,8 +44,8 @@ export function PasoDetallesPanel({
if (wizard.modoCreacion === 'IA') {
return (
<div className="grid gap-4">
<div>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<Label htmlFor="desc">Descripción del enfoque</Label>
<textarea
id="desc"
@@ -61,24 +63,8 @@ export function PasoDetallesPanel({
}
/>
</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>
<div className="flex flex-col gap-1">
<Label htmlFor="notas">Notas adicionales</Label>
<textarea
id="notas"
@@ -96,6 +82,7 @@ export function PasoDetallesPanel({
}
/>
</div>
<ReferenciasParaIA />
<div className="flex items-center justify-between">
<div className="text-muted-foreground text-sm">
Opcional: se pueden adjuntar recursos IA más adelante.

View File

@@ -0,0 +1,186 @@
import { Upload, File, X, FileText } from 'lucide-react'
import { useState, useCallback } from 'react'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
interface UploadedFile {
id: string
name: string
size: string
type: string
}
interface FileDropzoneProps {
onFilesChange?: (files: Array<UploadedFile>) => void
acceptedTypes?: string
maxFiles?: number
title?: string
description?: string
}
export function FileDropzone({
// onFilesChange,
acceptedTypes = '.doc,.docx,.pdf',
maxFiles = 5,
title = 'Arrastra archivos aquí',
description = 'o haz clic para seleccionar',
}: FileDropzoneProps) {
const [isDragging, setIsDragging] = useState(false)
const [files, setFiles] = useState<Array<UploadedFile>>([])
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault()
setIsDragging(true)
}, [])
const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault()
setIsDragging(false)
}, [])
const handleDrop = useCallback(
(e: React.DragEvent) => {
e.preventDefault()
setIsDragging(false)
const droppedFiles = Array.from(e.dataTransfer.files)
addFiles(droppedFiles)
},
[files],
)
const handleFileInput = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
const selectedFiles = Array.from(e.target.files)
addFiles(selectedFiles)
}
},
[files],
)
const addFiles = (newFiles: Array<File>) => {
const uploadedFiles: Array<UploadedFile> = newFiles
.slice(0, maxFiles - files.length)
.map((file) => ({
id: `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
name: file.name,
size: formatFileSize(file.size),
type: file.name.split('.').pop() || 'file',
}))
const updatedFiles = [...files, ...uploadedFiles].slice(0, maxFiles)
setFiles(updatedFiles)
// onFilesChange(updatedFiles)
}
const removeFile = (fileId: string) => {
const updatedFiles = files.filter((f) => f.id !== fileId)
setFiles(updatedFiles)
// onFilesChange(updatedFiles)
}
const formatFileSize = (bytes: number): string => {
if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
}
const getFileIcon = (type: string) => {
switch (type.toLowerCase()) {
case 'pdf':
return <FileText className="text-destructive h-4 w-4" />
case 'doc':
case 'docx':
return <FileText className="text-info h-4 w-4" />
default:
return <File className="text-muted-foreground h-4 w-4" />
}
}
return (
<div className="space-y-3">
<div
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={cn(
'border-border hover:border-primary/50 cursor-pointer rounded-xl border-2 border-dashed p-8 text-center transition-all duration-300',
isDragging && 'active',
)}
style={{ background: 'var(--gradient-subtle)' }}
>
<input
type="file"
accept={acceptedTypes}
multiple
onChange={handleFileInput}
className="hidden"
id="file-upload"
/>
<label htmlFor="file-upload" className="cursor-pointer">
<div className="flex flex-col items-center gap-3">
<div
className={cn(
'flex h-12 w-12 items-center justify-center rounded-xl transition-colors',
isDragging
? 'bg-primary text-primary-foreground'
: 'bg-accent text-accent-foreground',
)}
>
<Upload className="h-6 w-6" />
</div>
<div className="text-center">
<p className="text-foreground text-sm font-medium">{title}</p>
<p className="text-muted-foreground mt-1 text-xs">
{description}
</p>
<p className="text-muted-foreground mt-1 text-xs">
Formatos:{' '}
{acceptedTypes
.replace(/\./g, '')
.toUpperCase()
.replace(/,/g, ', ')}
</p>
</div>
</div>
</label>
</div>
{/* Uploaded files list */}
{files.length > 0 && (
<div className="space-y-2">
{files.map((file) => (
<div
key={file.id}
className="bg-accent/50 border-border fade-in flex items-center gap-3 rounded-lg border p-3"
>
{getFileIcon(file.type)}
<div className="min-w-0 flex-1">
<p className="text-foreground truncate text-sm font-medium">
{file.name}
</p>
<p className="text-muted-foreground text-xs">{file.size}</p>
</div>
<Button
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-destructive h-8 w-8"
onClick={() => removeFile(file.id)}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
{files.length >= maxFiles && (
<p className="text-warning text-center text-xs">
Máximo de {maxFiles} archivos alcanzado
</p>
)}
</div>
)
}

View File

@@ -0,0 +1,138 @@
import { FileText, FolderOpen, Upload } from 'lucide-react'
import { FileDropzone } from './FileDropZone'
import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label'
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
TabsContents,
} from '@/components/ui/motion-tabs'
import { ARCHIVOS, REPOSITORIOS } from '@/features/planes/nuevo/catalogs'
const tabs = [
{
name: 'Archivos existentes',
value: 'archivos-existentes',
icon: FileText,
content: (
<div className="flex flex-col gap-0.5">
{ARCHIVOS.map((archivo) => (
<Label
key={archivo.id}
className="border-border hover:border-primary/30 hover:bg-accent/50 m-0.5 flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors has-[[aria-checked=true]]:border-blue-600 has-[[aria-checked=true]]:bg-blue-50 dark:has-[[aria-checked=true]]:border-blue-900 dark:has-[[aria-checked=true]]:bg-blue-950"
>
<Checkbox className="peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" />
<FileText className="text-muted-foreground h-4 w-4" />
<div className="min-w-0 flex-1">
<p className="text-foreground truncate text-sm font-medium">
{archivo.nombre}
</p>
<p className="text-muted-foreground text-xs">{archivo.tamaño}</p>
</div>
</Label>
))}
</div>
),
},
{
name: 'Repositorios',
value: 'repositorios',
icon: FolderOpen,
content: (
<div className="flex flex-col gap-0.5">
{REPOSITORIOS.map((repositorio) => (
<Label
key={repositorio.id}
className="border-border hover:border-primary/30 hover:bg-accent/50 m-0.5 flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors has-[[aria-checked=true]]:border-blue-600 has-[[aria-checked=true]]:bg-blue-50 dark:has-[[aria-checked=true]]:border-blue-900 dark:has-[[aria-checked=true]]:bg-blue-950"
>
<Checkbox className="peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" />
<FolderOpen className="text-muted-foreground h-4 w-4" />
<div className="min-w-0 flex-1">
<p className="text-foreground text-sm font-medium">
{repositorio.nombre}
</p>
<p className="text-muted-foreground text-xs">
{repositorio.descripcion} · {repositorio.cantidadArchivos}{' '}
archivos
</p>
</div>
</Label>
))}
</div>
),
},
{
name: 'Subir archivos',
value: 'subir-archivos',
icon: Upload,
content: (
<div>
<FileDropzone
// onFilesChange={(files) =>
// handleChange("archivosAdhocIds", files.map((f) => f.id))
// }
title="Sube archivos de referencia"
description="Documentos que serán usados como contexto para la generación"
/>
</div>
),
},
]
const ReferenciasParaIA = () => {
return (
<div className="flex w-full flex-col gap-1">
<Label>Referencias para la IA</Label>
<Tabs defaultValue="archivos-existentes" className="gap-4">
<TabsList className="w-full">
{tabs.map(({ icon: Icon, name, value }) => (
<TabsTrigger
key={value}
value={value}
className="flex items-center gap-1 px-2.5 sm:px-3"
>
<Icon />
<span className="hidden sm:inline">{name}</span>
</TabsTrigger>
))}
</TabsList>
<TabsContents className="bg-background mx-1 -mt-2 mb-1 h-full rounded-sm">
{tabs.map((tab) => (
<TabsContent
key={tab.value}
value={tab.value}
className="animate-in fade-in duration-300 ease-out"
>
{tab.content}
</TabsContent>
))}
</TabsContents>
</Tabs>
</div>
)
}
export default ReferenciasParaIA

View File

@@ -4,7 +4,7 @@ import type {
NewPlanWizardState,
ModoCreacion,
SubModoClonado,
} from '@/features/planes/new/types'
} from '@/features/planes/nuevo/types'
import {
Card,
@@ -23,6 +23,19 @@ export function PasoModoCardGroup({
}) {
const isSelected = (m: ModoCreacion) => wizard.modoCreacion === m
const isSubSelected = (s: SubModoClonado) => wizard.subModoClonado === s
const handleKeyActivate = (e: React.KeyboardEvent, cb: () => void) => {
const key = e.key
if (
key === 'Enter' ||
key === ' ' ||
key === 'Spacebar' ||
key === 'Space'
) {
e.preventDefault()
e.stopPropagation()
cb()
}
}
return (
<div className="grid gap-4 sm:grid-cols-3">
<Card
@@ -34,6 +47,15 @@ export function PasoModoCardGroup({
subModoClonado: undefined,
}))
}
onKeyDown={(e: React.KeyboardEvent) =>
handleKeyActivate(e, () =>
onChange((w) => ({
...w,
modoCreacion: 'MANUAL',
subModoClonado: undefined,
})),
)
}
role="button"
tabIndex={0}
>
@@ -54,6 +76,15 @@ export function PasoModoCardGroup({
subModoClonado: undefined,
}))
}
onKeyDown={(e: React.KeyboardEvent) =>
handleKeyActivate(e, () =>
onChange((w) => ({
...w,
modoCreacion: 'IA',
subModoClonado: undefined,
})),
)
}
role="button"
tabIndex={0}
>
@@ -70,6 +101,11 @@ export function PasoModoCardGroup({
<Card
className={isSelected('CLONADO') ? 'ring-ring ring-2' : ''}
onClick={() => onChange((w) => ({ ...w, modoCreacion: 'CLONADO' }))}
onKeyDown={(e: React.KeyboardEvent) =>
handleKeyActivate(e, () =>
onChange((w) => ({ ...w, modoCreacion: 'CLONADO' })),
)
}
role="button"
tabIndex={0}
>
@@ -88,6 +124,11 @@ export function PasoModoCardGroup({
e.stopPropagation()
onChange((w) => ({ ...w, subModoClonado: 'INTERNO' }))
}}
onKeyDown={(e: React.KeyboardEvent) =>
handleKeyActivate(e, () =>
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'
@@ -105,6 +146,11 @@ export function PasoModoCardGroup({
e.stopPropagation()
onChange((w) => ({ ...w, subModoClonado: 'TRADICIONAL' }))
}}
onKeyDown={(e: React.KeyboardEvent) =>
handleKeyActivate(e, () =>
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'

View File

@@ -1,4 +1,4 @@
import type { NewPlanWizardState } from '@/features/planes/new/types'
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
import {
Card,

View File

@@ -7,7 +7,7 @@ const CheckboxCardDemo = () => {
<Label className="border-border hover:border-primary/30 hover:bg-accent/50 flex cursor-pointer items-center items-start gap-2 gap-3 rounded-lg border p-3 transition-colors has-[[aria-checked=true]]:border-blue-600 has-[[aria-checked=true]]:bg-blue-50 dark:has-[[aria-checked=true]]:border-blue-900 dark:has-[[aria-checked=true]]:bg-blue-950">
<Checkbox
defaultChecked
className="data-[state=checked]:border-blue-600 data-[state=checked]:bg-blue-600 data-[state=checked]:text-white dark:data-[state=checked]:border-blue-700 dark:data-[state=checked]:bg-blue-700"
className="peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50"
/>
<div className="grid gap-1.5 font-normal">
<p className="text-sm leading-none font-medium">Auto Start</p>
@@ -17,7 +17,7 @@ const CheckboxCardDemo = () => {
</div>
</Label>
<Label className="hover:bg-accent/50 flex items-start gap-2 rounded-lg border p-3 has-[[aria-checked=true]]:border-blue-600 has-[[aria-checked=true]]:bg-blue-50 dark:has-[[aria-checked=true]]:border-blue-900 dark:has-[[aria-checked=true]]:bg-blue-950">
<Checkbox className="data-[state=checked]:border-blue-600 data-[state=checked]:bg-blue-600 data-[state=checked]:text-white dark:data-[state=checked]:border-blue-700 dark:data-[state=checked]:bg-blue-700" />
<Checkbox className="peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" />
<div className="grid gap-1.5 font-normal">
<p className="text-sm leading-none font-medium">Auto update</p>
<p className="text-muted-foreground text-sm">

View File

@@ -0,0 +1,72 @@
import { Tabs, TabsContent, TabsContents, TabsList, TabsTrigger } from '@/components/ui/motion-tabs'
const tabs = [
{
name: 'Explore',
value: 'explore',
content: (
<>
Discover <span className='text-foreground font-semibold'>fresh ideas</span>, trending topics, and hidden gems
curated just for you. Start exploring and let your curiosity lead the way!
</>
)
},
{
name: 'Favorites',
value: 'favorites',
content: (
<>
All your <span className='text-foreground font-semibold'>favorites</span> are saved here. Revisit articles,
collections, and moments you love, any time you want a little inspiration.
</>
)
},
{
name: 'Surprise Me',
value: 'surprise',
content: (
<>
<span className='text-foreground font-semibold'>Surprise!</span> Here&apos;s something unexpecteda fun fact, a
quirky tip, or a daily challenge. Come back for a new surprise every day!
</>
)
}
]
const AnimatedTabsDemo = () => {
return (
<div className='w-full max-w-md'>
<Tabs defaultValue='explore' className='gap-4'>
<TabsList>
{tabs.map(tab => (
<TabsTrigger key={tab.value} value={tab.value}>
{tab.name}
</TabsTrigger>
))}
</TabsList>
<TabsContents className='bg-background mx-1 -mt-2 mb-1 h-full rounded-sm'>
{tabs.map(tab => (
<TabsContent key={tab.value} value={tab.value}>
<p className='text-muted-foreground text-sm'>{tab.content}</p>
</TabsContent>
))}
</TabsContents>
</Tabs>
<p className='text-muted-foreground mt-4 text-center text-xs'>
Inspired by{' '}
<a
className='hover:text-foreground underline'
href='https://animate-ui.com/docs/components/tabs'
target='_blank'
rel='noopener noreferrer'
>
Animate UI
</a>
</p>
</div>
)
}
export default AnimatedTabsDemo

View File

@@ -0,0 +1,549 @@
import * as React from 'react'
import type { Transition } from 'motion/react'
import { AnimatePresence, motion } from 'motion/react'
import { cn } from '@/lib/utils'
type MotionHighlightMode = 'children' | 'parent'
type Bounds = {
top: number
left: number
width: number
height: number
}
type MotionHighlightContextType<T extends string> = {
mode: MotionHighlightMode
activeValue: T | null
setActiveValue: (value: T | null) => void
setBounds: (bounds: DOMRect) => void
clearBounds: () => void
id: string
hover: boolean
className?: string
activeClassName?: string
setActiveClassName: (className: string) => void
transition?: Transition
disabled?: boolean
enabled?: boolean
exitDelay?: number
forceUpdateBounds?: boolean
}
const MotionHighlightContext = React.createContext<
// eslint-disable-next-line @typescript-eslint/no-explicit-any
MotionHighlightContextType<any> | undefined
>(undefined)
function useMotionHighlight<T extends string>(): MotionHighlightContextType<T> {
const context = React.useContext(MotionHighlightContext)
if (!context) {
throw new Error('useMotionHighlight must be used within a MotionHighlightProvider')
}
return context as unknown as MotionHighlightContextType<T>
}
type BaseMotionHighlightProps<T extends string> = {
mode?: MotionHighlightMode
value?: T | null
defaultValue?: T | null
onValueChange?: (value: T | null) => void
className?: string
transition?: Transition
hover?: boolean
disabled?: boolean
enabled?: boolean
exitDelay?: number
}
type ParentModeMotionHighlightProps = {
boundsOffset?: Partial<Bounds>
containerClassName?: string
forceUpdateBounds?: boolean
}
type ControlledParentModeMotionHighlightProps<T extends string> = BaseMotionHighlightProps<T> &
ParentModeMotionHighlightProps & {
mode: 'parent'
controlledItems: true
children: React.ReactNode
}
type ControlledChildrenModeMotionHighlightProps<T extends string> = BaseMotionHighlightProps<T> & {
mode?: 'children' | undefined
controlledItems: true
children: React.ReactNode
}
type UncontrolledParentModeMotionHighlightProps<T extends string> = BaseMotionHighlightProps<T> &
ParentModeMotionHighlightProps & {
mode: 'parent'
controlledItems?: false
itemsClassName?: string
children: React.ReactElement | React.ReactElement[]
}
type UncontrolledChildrenModeMotionHighlightProps<T extends string> = BaseMotionHighlightProps<T> & {
mode?: 'children'
controlledItems?: false
itemsClassName?: string
children: React.ReactElement | React.ReactElement[]
}
type MotionHighlightProps<T extends string> = React.ComponentProps<'div'> &
(
| ControlledParentModeMotionHighlightProps<T>
| ControlledChildrenModeMotionHighlightProps<T>
| UncontrolledParentModeMotionHighlightProps<T>
| UncontrolledChildrenModeMotionHighlightProps<T>
)
function MotionHighlight<T extends string>({ ref, ...props }: MotionHighlightProps<T>) {
const {
children,
value,
defaultValue,
onValueChange,
className,
transition = { type: 'spring', stiffness: 350, damping: 35 },
hover = false,
enabled = true,
controlledItems,
disabled = false,
exitDelay = 0.2,
mode = 'children'
} = props
const localRef = React.useRef<HTMLDivElement>(null)
React.useImperativeHandle(ref, () => localRef.current as HTMLDivElement)
const [activeValue, setActiveValue] = React.useState<T | null>(value ?? defaultValue ?? null)
const [boundsState, setBoundsState] = React.useState<Bounds | null>(null)
const [activeClassNameState, setActiveClassNameState] = React.useState<string>('')
const safeSetActiveValue = React.useCallback(
(id: T | null) => {
setActiveValue(prev => (prev === id ? prev : id))
if (id !== activeValue) onValueChange?.(id as T)
},
[activeValue, onValueChange]
)
const safeSetBounds = React.useCallback(
(bounds: DOMRect) => {
if (!localRef.current) return
const boundsOffset = (props as ParentModeMotionHighlightProps)?.boundsOffset ?? {
top: 0,
left: 0,
width: 0,
height: 0
}
const containerRect = localRef.current.getBoundingClientRect()
const newBounds: Bounds = {
top: bounds.top - containerRect.top + (boundsOffset.top ?? 0),
left: bounds.left - containerRect.left + (boundsOffset.left ?? 0),
width: bounds.width + (boundsOffset.width ?? 0),
height: bounds.height + (boundsOffset.height ?? 0)
}
setBoundsState(prev => {
if (
prev &&
prev.top === newBounds.top &&
prev.left === newBounds.left &&
prev.width === newBounds.width &&
prev.height === newBounds.height
) {
return prev
}
return newBounds
})
},
[props]
)
const clearBounds = React.useCallback(() => {
setBoundsState(prev => (prev === null ? prev : null))
}, [])
React.useEffect(() => {
if (value !== undefined) setActiveValue(value)
else if (defaultValue !== undefined) setActiveValue(defaultValue)
}, [value, defaultValue])
const id = React.useId()
React.useEffect(() => {
if (mode !== 'parent') return
const container = localRef.current
if (!container) return
const onScroll = () => {
if (!activeValue) return
const activeEl = container.querySelector<HTMLElement>(`[data-value="${activeValue}"][data-highlight="true"]`)
if (activeEl) safeSetBounds(activeEl.getBoundingClientRect())
}
container.addEventListener('scroll', onScroll, { passive: true })
return () => container.removeEventListener('scroll', onScroll)
}, [mode, activeValue, safeSetBounds])
const render = React.useCallback(
(children: React.ReactNode) => {
if (mode === 'parent') {
return (
<div
ref={localRef}
data-slot='motion-highlight-container'
className={cn('relative', (props as ParentModeMotionHighlightProps)?.containerClassName)}
>
<AnimatePresence initial={false}>
{boundsState && (
<motion.div
data-slot='motion-highlight'
animate={{
top: boundsState.top,
left: boundsState.left,
width: boundsState.width,
height: boundsState.height,
opacity: 1
}}
initial={{
top: boundsState.top,
left: boundsState.left,
width: boundsState.width,
height: boundsState.height,
opacity: 0
}}
exit={{
opacity: 0,
transition: {
...transition,
delay: (transition?.delay ?? 0) + (exitDelay ?? 0)
}
}}
transition={transition}
className={cn('bg-muted absolute z-0', className, activeClassNameState)}
/>
)}
</AnimatePresence>
{children}
</div>
)
}
return children
},
[mode, props, boundsState, transition, exitDelay, className, activeClassNameState]
)
return (
<MotionHighlightContext.Provider
value={{
mode,
activeValue,
setActiveValue: safeSetActiveValue,
id,
hover,
className,
transition,
disabled,
enabled,
exitDelay,
setBounds: safeSetBounds,
clearBounds,
activeClassName: activeClassNameState,
setActiveClassName: setActiveClassNameState,
forceUpdateBounds: (props as ParentModeMotionHighlightProps)?.forceUpdateBounds
}}
>
{enabled
? controlledItems
? render(children)
: render(
React.Children.map(children, (child, index) => (
<MotionHighlightItem key={index} className={props?.itemsClassName}>
{child}
</MotionHighlightItem>
))
)
: children}
</MotionHighlightContext.Provider>
)
}
function getNonOverridingDataAttributes(
element: React.ReactElement,
dataAttributes: Record<string, unknown>
): Record<string, unknown> {
return Object.keys(dataAttributes).reduce<Record<string, unknown>>((acc, key) => {
if ((element.props as Record<string, unknown>)[key] === undefined) {
acc[key] = dataAttributes[key]
}
return acc
}, {})
}
type ExtendedChildProps = React.ComponentProps<'div'> & {
id?: string
ref?: React.Ref<HTMLElement>
'data-active'?: string
'data-value'?: string
'data-disabled'?: boolean
'data-highlight'?: boolean
'data-slot'?: string
}
type MotionHighlightItemProps = React.ComponentProps<'div'> & {
children: React.ReactElement
id?: string
value?: string
className?: string
transition?: Transition
activeClassName?: string
disabled?: boolean
exitDelay?: number
asChild?: boolean
forceUpdateBounds?: boolean
}
function MotionHighlightItem({
ref,
children,
id,
value,
className,
transition,
disabled = false,
activeClassName,
exitDelay,
asChild = false,
forceUpdateBounds,
...props
}: MotionHighlightItemProps) {
const itemId = React.useId()
const {
activeValue,
setActiveValue,
mode,
setBounds,
clearBounds,
hover,
enabled,
className: contextClassName,
transition: contextTransition,
id: contextId,
disabled: contextDisabled,
exitDelay: contextExitDelay,
forceUpdateBounds: contextForceUpdateBounds,
setActiveClassName
} = useMotionHighlight()
const element = children as React.ReactElement<ExtendedChildProps>
const childValue = id ?? value ?? element.props?.['data-value'] ?? element.props?.id ?? itemId
const isActive = activeValue === childValue
const isDisabled = disabled === undefined ? contextDisabled : disabled
const itemTransition = transition ?? contextTransition
const localRef = React.useRef<HTMLDivElement>(null)
React.useImperativeHandle(ref, () => localRef.current as HTMLDivElement)
React.useEffect(() => {
if (mode !== 'parent') return
let rafId: number
let previousBounds: Bounds | null = null
const shouldUpdateBounds = forceUpdateBounds === true || (contextForceUpdateBounds && forceUpdateBounds !== false)
const updateBounds = () => {
if (!localRef.current) return
const bounds = localRef.current.getBoundingClientRect()
if (shouldUpdateBounds) {
if (
previousBounds &&
previousBounds.top === bounds.top &&
previousBounds.left === bounds.left &&
previousBounds.width === bounds.width &&
previousBounds.height === bounds.height
) {
rafId = requestAnimationFrame(updateBounds)
return
}
previousBounds = bounds
rafId = requestAnimationFrame(updateBounds)
}
setBounds(bounds)
}
if (isActive) {
updateBounds()
setActiveClassName(activeClassName ?? '')
} else if (!activeValue) clearBounds()
if (shouldUpdateBounds) return () => cancelAnimationFrame(rafId)
}, [
mode,
isActive,
activeValue,
setBounds,
clearBounds,
activeClassName,
setActiveClassName,
forceUpdateBounds,
contextForceUpdateBounds
])
if (!React.isValidElement(children)) return children
const dataAttributes = {
'data-active': isActive ? 'true' : 'false',
'aria-selected': isActive,
'data-disabled': isDisabled,
'data-value': childValue,
'data-highlight': true
}
const commonHandlers = hover
? {
onMouseEnter: (e: React.MouseEvent<HTMLDivElement>) => {
setActiveValue(childValue)
element.props.onMouseEnter?.(e)
},
onMouseLeave: (e: React.MouseEvent<HTMLDivElement>) => {
setActiveValue(null)
element.props.onMouseLeave?.(e)
}
}
: {
onClick: (e: React.MouseEvent<HTMLDivElement>) => {
setActiveValue(childValue)
element.props.onClick?.(e)
}
}
if (asChild) {
if (mode === 'children') {
return React.cloneElement(
element,
{
key: childValue,
ref: localRef,
className: cn('relative', element.props.className),
...getNonOverridingDataAttributes(element, {
...dataAttributes,
'data-slot': 'motion-highlight-item-container'
}),
...commonHandlers,
...props
},
<>
<AnimatePresence initial={false}>
{isActive && !isDisabled && (
<motion.div
layoutId={`transition-background-${contextId}`}
data-slot='motion-highlight'
className={cn('bg-muted absolute inset-0 z-0', contextClassName, activeClassName)}
transition={itemTransition}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{
opacity: 0,
transition: {
...itemTransition,
delay: (itemTransition?.delay ?? 0) + (exitDelay ?? contextExitDelay ?? 0)
}
}}
{...dataAttributes}
/>
)}
</AnimatePresence>
<div data-slot='motion-highlight-item' className={cn('relative z-[1]', className)} {...dataAttributes}>
{children}
</div>
</>
)
}
return React.cloneElement(element, {
ref: localRef,
...getNonOverridingDataAttributes(element, {
...dataAttributes,
'data-slot': 'motion-highlight-item'
}),
...commonHandlers
})
}
return enabled ? (
<div
key={childValue}
ref={localRef}
data-slot='motion-highlight-item-container'
className={cn(mode === 'children' && 'relative', className)}
{...dataAttributes}
{...props}
{...commonHandlers}
>
{mode === 'children' && (
<AnimatePresence initial={false}>
{isActive && !isDisabled && (
<motion.div
layoutId={`transition-background-${contextId}`}
data-slot='motion-highlight'
className={cn('bg-muted absolute inset-0 z-0', contextClassName, activeClassName)}
transition={itemTransition}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{
opacity: 0,
transition: {
...itemTransition,
delay: (itemTransition?.delay ?? 0) + (exitDelay ?? contextExitDelay ?? 0)
}
}}
{...dataAttributes}
/>
)}
</AnimatePresence>
)}
{React.cloneElement(element, {
className: cn('relative z-[1]', element.props.className),
...getNonOverridingDataAttributes(element, {
...dataAttributes,
'data-slot': 'motion-highlight-item'
})
})}
</div>
) : (
children
)
}
export {
MotionHighlight,
MotionHighlightItem,
useMotionHighlight,
type MotionHighlightProps,
type MotionHighlightItemProps
}

View File

@@ -0,0 +1,261 @@
'use client'
import * as React from 'react'
import { motion, type Transition, type HTMLMotionProps } from 'motion/react'
import { cn } from '@/lib/utils'
import { MotionHighlight, MotionHighlightItem } from '@/components/ui/motion-highlight'
type TabsContextType<T extends string> = {
activeValue: T
handleValueChange: (value: T) => void
registerTrigger: (value: T, node: HTMLElement | null) => void
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const TabsContext = React.createContext<TabsContextType<any> | undefined>(undefined)
function useTabs<T extends string = string>(): TabsContextType<T> {
const context = React.useContext(TabsContext)
if (!context) {
throw new Error('useTabs must be used within a TabsProvider')
}
return context
}
type BaseTabsProps = React.ComponentProps<'div'> & {
children: React.ReactNode
}
type UnControlledTabsProps<T extends string = string> = BaseTabsProps & {
defaultValue?: T
value?: never
onValueChange?: never
}
type ControlledTabsProps<T extends string = string> = BaseTabsProps & {
value: T
onValueChange?: (value: T) => void
defaultValue?: never
}
type TabsProps<T extends string = string> = UnControlledTabsProps<T> | ControlledTabsProps<T>
function Tabs<T extends string = string>({
defaultValue,
value,
onValueChange,
children,
className,
...props
}: TabsProps<T>) {
const [activeValue, setActiveValue] = React.useState<T | undefined>(defaultValue ?? undefined)
const triggersRef = React.useRef(new Map<string, HTMLElement>())
const initialSet = React.useRef(false)
const isControlled = value !== undefined
React.useEffect(() => {
if (!isControlled && activeValue === undefined && triggersRef.current.size > 0 && !initialSet.current) {
const firstTab = Array.from(triggersRef.current.keys())[0]
setActiveValue(firstTab as T)
initialSet.current = true
}
}, [activeValue, isControlled])
const registerTrigger = (value: string, node: HTMLElement | null) => {
if (node) {
triggersRef.current.set(value, node)
if (!isControlled && activeValue === undefined && !initialSet.current) {
setActiveValue(value as T)
initialSet.current = true
}
} else {
triggersRef.current.delete(value)
}
}
const handleValueChange = (val: T) => {
if (!isControlled) setActiveValue(val)
else onValueChange?.(val)
}
return (
<TabsContext.Provider
value={{
activeValue: (value ?? activeValue)!,
handleValueChange,
registerTrigger
}}
>
<div data-slot='tabs' className={cn('flex flex-col gap-2', className)} {...props}>
{children}
</div>
</TabsContext.Provider>
)
}
type TabsListProps = React.ComponentProps<'div'> & {
children: React.ReactNode
activeClassName?: string
transition?: Transition
}
function TabsList({
children,
className,
activeClassName,
transition = {
type: 'spring',
stiffness: 200,
damping: 25
},
...props
}: TabsListProps) {
const { activeValue } = useTabs()
return (
<MotionHighlight
controlledItems
className={cn('bg-background rounded-sm shadow-sm', activeClassName)}
value={activeValue}
transition={transition}
>
<div
role='tablist'
data-slot='tabs-list'
className={cn(
'bg-muted text-muted-foreground inline-flex h-10 w-fit items-center justify-center rounded-lg p-[4px]',
className
)}
{...props}
>
{children}
</div>
</MotionHighlight>
)
}
type TabsTriggerProps = HTMLMotionProps<'button'> & {
value: string
children: React.ReactNode
}
function TabsTrigger({ ref, value, children, className, ...props }: TabsTriggerProps) {
const { activeValue, handleValueChange, registerTrigger } = useTabs()
const localRef = React.useRef<HTMLButtonElement | null>(null)
React.useImperativeHandle(ref, () => localRef.current as HTMLButtonElement)
React.useEffect(() => {
registerTrigger(value, localRef.current)
return () => registerTrigger(value, null)
}, [value, registerTrigger])
return (
<MotionHighlightItem value={value} className='size-full'>
<motion.button
ref={localRef}
data-slot='tabs-trigger'
role='tab'
onClick={() => handleValueChange(value)}
data-state={activeValue === value ? 'active' : 'inactive'}
className={cn(
'ring-offset-background focus-visible:ring-ring data-[state=active]:text-foreground z-[1] inline-flex size-full cursor-pointer items-center justify-center rounded-sm px-2 py-1 text-sm font-medium whitespace-nowrap transition-transform focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50',
className
)}
{...props}
>
{children}
</motion.button>
</MotionHighlightItem>
)
}
type TabsContentsProps = React.ComponentProps<'div'> & {
children: React.ReactNode
transition?: Transition
}
function TabsContents({
children,
className,
transition = {
type: 'spring',
stiffness: 300,
damping: 30,
bounce: 0,
restDelta: 0.01
},
...props
}: TabsContentsProps) {
const { activeValue } = useTabs()
const childrenArray = React.Children.toArray(children)
const activeIndex = childrenArray.findIndex(
(child): child is React.ReactElement<{ value: string }> =>
React.isValidElement(child) &&
typeof child.props === 'object' &&
child.props !== null &&
'value' in child.props &&
child.props.value === activeValue
)
return (
<div data-slot='tabs-contents' className={cn('overflow-hidden', className)} {...props}>
<motion.div className='-mx-2 flex' animate={{ x: activeIndex * -100 + '%' }} transition={transition}>
{childrenArray.map((child, index) => (
<div key={index} className='w-full shrink-0 px-2'>
{child}
</div>
))}
</motion.div>
</div>
)
}
type TabsContentProps = HTMLMotionProps<'div'> & {
value: string
children: React.ReactNode
}
function TabsContent({ children, value, className, ...props }: TabsContentProps) {
const { activeValue } = useTabs()
const isActive = activeValue === value
return (
<motion.div
role='tabpanel'
data-slot='tabs-content'
className={cn('overflow-hidden', className)}
initial={{ filter: 'blur(0px)' }}
animate={{ filter: isActive ? 'blur(0px)' : 'blur(2px)' }}
exit={{ filter: 'blur(0px)' }}
transition={{ type: 'spring', stiffness: 300, damping: 20 }}
{...props}
>
{children}
</motion.div>
)
}
export {
Tabs,
TabsList,
TabsTrigger,
TabsContents,
TabsContent,
useTabs,
type TabsContextType,
type TabsProps,
type TabsListProps,
type TabsTriggerProps,
type TabsContentsProps,
type TabsContentProps
}

View File

@@ -1,127 +0,0 @@
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>
)
}

View File

@@ -1,56 +0,0 @@
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" },
];

View File

@@ -1,90 +0,0 @@
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,
};
}

View File

@@ -1,45 +0,0 @@
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;
};

View File

@@ -1,205 +0,0 @@
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>
)
}

View File

@@ -1,56 +0,0 @@
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",
},
];

View File

@@ -1,102 +0,0 @@
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,
};
}

View File

@@ -1,41 +0,0 @@
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;
};

View File

@@ -9,12 +9,12 @@ export function useNuevoPlanWizard() {
step: 1,
modoCreacion: null,
datosBasicos: {
nombrePlan: "",
carreraId: "",
facultadId: "",
nivel: "",
tipoCiclo: "",
numCiclos: undefined,
nombrePlan: "Medicina",
carreraId: "medico",
facultadId: "med",
nivel: "Licenciatura",
tipoCiclo: "SEMESTRE",
numCiclos: 8,
},
clonInterno: { planOrigenId: null },
clonTradicional: {

View File

@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
import { NuevaAsignaturaModalContainer } from '@/features/asignaturas/new/NuevaAsignaturaModalContainer'
import { NuevaAsignaturaModalContainer } from '@/features/asignaturas/nueva/NuevaAsignaturaModalContainer'
export const Route = createFileRoute(
'/planes/$planId/asignaturas/_lista/nueva',

View File

@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
import NuevoPlanModalContainer from '@/features/planes/new/NuevoPlanModalContainer'
import NuevoPlanModalContainer from '@/features/planes/nuevo/NuevoPlanModalContainer'
export const Route = createFileRoute('/planes/_lista/nuevo')({
component: NuevoPlanModalContainer,