Compare commits

..
13 Commits
16 changed files with 2019 additions and 963 deletions

No files matched your search

+737 -14
View File
@@ -9,6 +9,8 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@azure/msal-browser": "^5.21.0", "@azure/msal-browser": "^5.21.0",
"@heroui/react": "^3.2.4",
"@heroui/styles": "^3.2.4",
"@react-oauth/google": "^0.13.5", "@react-oauth/google": "^0.13.5",
"@tailwindcss/postcss": "^4.3.3", "@tailwindcss/postcss": "^4.3.3",
"@tanstack/react-query": "^5.101.4", "@tanstack/react-query": "^5.101.4",
@@ -35,6 +37,50 @@
"typescript-eslint": "^8.65.0" "typescript-eslint": "^8.65.0"
} }
}, },
"node_modules/@adobe/react-spectrum": {
"version": "3.47.5",
"resolved": "https://registry.npmjs.org/@adobe/react-spectrum/-/react-spectrum-3.47.5.tgz",
"integrity": "sha512-iTbAwwMijVnQjwjwbvkWn82AyRAbFNSJzt/VawWp1vXRIxVIO9COZ1pPG5Fj88xXgqlG8maVcv58p74vqqy5HA==",
"license": "Apache-2.0",
"dependencies": {
"@internationalized/date": "^3.12.4",
"@react-types/shared": "^3.36.1",
"@spectrum-icons/ui": "^3.7.2",
"@spectrum-icons/workflow": "^4.3.2",
"@swc/helpers": "^0.5.0",
"client-only": "^0.0.1",
"clsx": "^2.0.0",
"react-aria": "3.52.1",
"react-aria-components": "1.21.1",
"react-stately": "3.50.0",
"react-transition-group": "^4.4.5",
"use-sync-external-store": "^1.6.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@adobe/react-spectrum-ui": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@adobe/react-spectrum-ui/-/react-spectrum-ui-1.2.1.tgz",
"integrity": "sha512-wcrbEE2O/9WnEn6avBnaVRRx88S5PLFsPLr4wffzlbMfXeQsy+RMQwaJd3cbzrn18/j04Isit7f7Emfn0dhrJA==",
"license": "Apache-2.0",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@adobe/react-spectrum-workflow": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@adobe/react-spectrum-workflow/-/react-spectrum-workflow-2.3.5.tgz",
"integrity": "sha512-b53VIPwPWKb/T5gzE3qs+QlGP5gVrw/LnWV3xMksDU+CRl3rzOKUwxIGiZO8ICyYh1WiyqY4myGlPU/nAynBUg==",
"license": "Apache-2.0",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@alloc/quick-lru": { "node_modules/@alloc/quick-lru": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -99,7 +145,6 @@
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.7", "@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7", "@babel/generator": "^7.29.7",
@@ -261,6 +306,15 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.29.7", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
@@ -447,6 +501,101 @@
"node": "^20.19.0 || ^22.13.0 || >=24" "node": "^20.19.0 || ^22.13.0 || >=24"
} }
}, },
"node_modules/@formatjs/ecma402-abstract": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz",
"integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/intl-localematcher": "0.6.2",
"decimal.js": "^10.4.3",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/fast-memoize": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
"integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/icu-messageformat-parser": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz",
"integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.6",
"@formatjs/icu-skeleton-parser": "1.8.16",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/icu-skeleton-parser": {
"version": "1.8.16",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz",
"integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.6",
"tslib": "^2.8.0"
}
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz",
"integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@heroui/react": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@heroui/react/-/react-3.2.4.tgz",
"integrity": "sha512-ZXCTyz1G7bgCdHVxRPy4vIRI/wdb0ZvzEUJGG7CAEp0zqQsKrTH1o9i3Ns8yM6EidtAyfMosULN29gFA3wegBA==",
"license": "MIT",
"dependencies": {
"@heroui/styles": "3.2.4",
"@radix-ui/react-avatar": "1.1.11",
"@react-stately/utils": "3.12.1",
"@react-types/color": "3.2.0",
"@react-types/shared": "3.36.1",
"input-otp": "1.4.2",
"tailwind-merge": "3.4.0",
"tailwind-variants": "3.3.1"
},
"peerDependencies": {
"@react-aria/i18n": "^3.13.1",
"@react-aria/ssr": "^3.10.1",
"@react-aria/utils": "^3.34.1",
"react": ">=19.0.0",
"react-aria": "^3.51.0",
"react-aria-components": "^1.20.0",
"react-dom": ">=19.0.0",
"tailwindcss": ">=4.0.0"
}
},
"node_modules/@heroui/styles": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@heroui/styles/-/styles-3.2.4.tgz",
"integrity": "sha512-tjiT7VFUSjw7TA6ZjeT+D3oX62+D27l7dhPwW+emwPnkdkRoKALFJEPBsxpmEI/x18uNkKFIoTk85bICyOO8Vg==",
"license": "MIT",
"dependencies": {
"tailwind-variants": "3.3.1",
"tw-animate-css": "1.4.0"
},
"peerDependencies": {
"tailwindcss": ">=4.0.0"
}
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.2", "version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -1014,6 +1163,44 @@
"url": "https://opencollective.com/libvips" "url": "https://opencollective.com/libvips"
} }
}, },
"node_modules/@internationalized/date": {
"version": "3.12.4",
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.12.4.tgz",
"integrity": "sha512-M1dEn4c1U1HsSlaVR8upZtSqvXrTkHDfv18H01uCSJyjVLDxnBR38v/fMxecmlwXKR4i9HeZcmgQAPE6A+aGJQ==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
},
"node_modules/@internationalized/message": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/@internationalized/message/-/message-3.1.10.tgz",
"integrity": "sha512-nc0Or6EdWHqZRcsXb6P9hBIpLsfSl/ILh0rk5h/OVBpzmhdExXtPy2cQtWsq8XKRBpRHwDNnAHt4OpolcB7dog==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@swc/helpers": "^0.5.0",
"intl-messageformat": "^10.1.0"
}
},
"node_modules/@internationalized/number": {
"version": "3.6.8",
"resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.8.tgz",
"integrity": "sha512-8UmMFia46DUt+k97zKd9fKWXcWHR+k8ae3eYzILETuT2KbIvLyOfac7zesw+sJdRAAZ7Q9pM1Mk22aXp2LD0Ig==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
},
"node_modules/@internationalized/string": {
"version": "3.2.10",
"resolved": "https://registry.npmjs.org/@internationalized/string/-/string-3.2.10.tgz",
"integrity": "sha512-PDx6//vHSpRnHfxqMqto11zQvhsaU74O3mKv2F/0eicGZcl9NLjQmGlbHz/LsJh5tLKp4A4L7ZVTzN1/MmMTvA==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
},
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13", "version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -1193,6 +1380,218 @@
"node": ">= 10" "node": ">= 10"
} }
}, },
"node_modules/@radix-ui/react-avatar": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz",
"integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==",
"license": "MIT",
"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"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-context": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz",
"integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-primitive": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
"integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
"license": "MIT",
"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"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
"integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
"license": "MIT",
"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"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-is-hydrated": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
"integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
"license": "MIT",
"dependencies": {
"use-sync-external-store": "^1.5.0"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@react-aria/color": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@react-aria/color/-/color-3.2.1.tgz",
"integrity": "sha512-v/pZh1yBNeayQ5Kio0xRIX0gBf6uu+KjCRW4QSydTyRpmhvk6sMzHT9i9/vIMdaDHaYHBXlXF+Tp/mvS9CG4Zw==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0",
"react-aria": "^3.48.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/i18n": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.13.1.tgz",
"integrity": "sha512-z56ZYcbfpNmMyiGLhyEjytpmEfoTlBaksk84q4kds3HvNkf7QWKj+DJVfVDrJX+c1LyuBsszLSX7yxJRiHsYKQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@internationalized/date": "^3.12.1",
"@internationalized/message": "^3.1.9",
"@internationalized/string": "^3.2.8",
"@swc/helpers": "^0.5.0",
"react-aria": "^3.48.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/ssr": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.10.1.tgz",
"integrity": "sha512-jn038/ZYmu6DpfXJ6r2U9zFFppjbc9wnApPJSCxao2RZVEqep4YyoniHSy8qv6V21/xyS4IV7W9a+X2jOjSuag==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@swc/helpers": "^0.5.0",
"react-aria": "^3.48.0"
},
"engines": {
"node": ">= 12"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-aria/utils": {
"version": "3.34.1",
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.34.1.tgz",
"integrity": "sha512-H6+rGZL+0f58bBNaUMfctEnT+NogqwAk+nHiB8sR3K+YlQ37GTuCijy2U/pPvQtFMS5mURrjZeBH5JNNXsx14A==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@swc/helpers": "^0.5.0",
"react-aria": "^3.48.0",
"react-stately": "^3.46.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-oauth/google": { "node_modules/@react-oauth/google": {
"version": "0.13.5", "version": "0.13.5",
"resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.13.5.tgz", "resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.13.5.tgz",
@@ -1203,6 +1602,120 @@
"react-dom": ">=16.8.0" "react-dom": ">=16.8.0"
} }
}, },
"node_modules/@react-spectrum/color": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/@react-spectrum/color/-/color-3.2.1.tgz",
"integrity": "sha512-FbB+GaVufsN+obNJmUAe8XGZTeJjEWvEf/k/7nxDxoDCBdg/o6eEPONAZyID5Dj1f8BZKNym8VURxDZx7IIyuQ==",
"license": "Apache-2.0",
"dependencies": {
"@adobe/react-spectrum": "^3.47.0",
"@swc/helpers": "^0.5.0",
"react-stately": "^3.46.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-spectrum/provider": {
"version": "3.11.1",
"resolved": "https://registry.npmjs.org/@react-spectrum/provider/-/provider-3.11.1.tgz",
"integrity": "sha512-TsoNdVdmlQ7L+75ILq5Yb3+wp/I1AtIeat0o+Y+ZBxP+TtWpwT1ZtCB5l3cplFVzHzOpZlzO0VaDrDP9ElGYDw==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@adobe/react-spectrum": "^3.47.0",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-stately/color": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/@react-stately/color/-/color-3.10.1.tgz",
"integrity": "sha512-v4YlLa/oRQpPa/M9Yq1e0Bl8Z7xzGZdNDAIbfIMZuiJtacaOKriPW+AOgjjT/Ab0fNX2YDbwIYvkiMOJBnTi5A==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0",
"react-stately": "^3.46.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-stately/utils": {
"version": "3.12.1",
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.12.1.tgz",
"integrity": "sha512-NqKfzrknpfwiewx7R2vk1P+CneClInPDsIhw15+jOcUYSEfej0nta4cJywuKQJ2gsPwqX/ojDNixedCve9FWGw==",
"license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0",
"react-stately": "^3.46.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-types/color": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@react-types/color/-/color-3.2.0.tgz",
"integrity": "sha512-beV3vz80nzZ1EuYUM7296Kyi3AHcMrbQw0qub/9yzHWVTKKc5sy/e4dCMKcWL/ArkeAyc7jDOiui190RQ4l0Fw==",
"license": "Apache-2.0",
"dependencies": {
"@react-aria/color": "^3.2.0",
"@react-spectrum/color": "^3.2.0",
"@react-stately/color": "^3.10.0"
},
"peerDependencies": {
"@react-spectrum/provider": "^3.0.0",
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@react-types/shared": {
"version": "3.36.1",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.36.1.tgz",
"integrity": "sha512-AzsuD9OfxTOZMMvTRhlN3oHBwOmFN7tDh27LzqmHt4+uOgPhJT7ZM7/kVs/8/o0WxayMUIk3hBmCFRHv1FUoag==",
"license": "Apache-2.0",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@spectrum-icons/ui": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/@spectrum-icons/ui/-/ui-3.7.2.tgz",
"integrity": "sha512-7IuSHO+Ck8gJUIGIXqDcDIU4R8RHRUZhlfz8AwN1rSdCEvdgp6DWYN0LDTMrWh7TN/H5hrJ7oDO/gUpR3TnBjw==",
"license": "Apache-2.0",
"dependencies": {
"@adobe/react-spectrum-ui": "1.2.1",
"@babel/runtime": "^7.24.4",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"@adobe/react-spectrum": "^3.47.0",
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@spectrum-icons/workflow": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/@spectrum-icons/workflow/-/workflow-4.3.2.tgz",
"integrity": "sha512-UkVg8Kp9rInjfqtG27in9p7RWfN9JFSvZr7ZOpN2n9jkrQ1y88hZpK8dKce8lgVb7jALXtsi2p0rjyaWzO0QsA==",
"license": "Apache-2.0",
"dependencies": {
"@adobe/react-spectrum-workflow": "2.3.5",
"@swc/helpers": "^0.5.0"
},
"peerDependencies": {
"@adobe/react-spectrum": "^3.47.0",
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/@swc/helpers": { "node_modules/@swc/helpers": {
"version": "0.5.23", "version": "0.5.23",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
@@ -1531,7 +2044,6 @@
"integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.2.2" "csstype": "^3.2.2"
} }
@@ -1540,7 +2052,7 @@
"version": "19.2.4", "version": "19.2.4",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
@@ -1591,7 +2103,6 @@
"integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==", "integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.67.0", "@typescript-eslint/scope-manager": "8.67.0",
"@typescript-eslint/types": "8.67.0", "@typescript-eslint/types": "8.67.0",
@@ -1796,7 +2307,6 @@
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -1843,6 +2353,18 @@
"url": "https://github.com/sponsors/epoberezkin" "url": "https://github.com/sponsors/epoberezkin"
} }
}, },
"node_modules/aria-hidden": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
"integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/asynckit": { "node_modules/asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -1916,7 +2438,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.11.12", "baseline-browser-mapping": "^2.11.12",
"caniuse-lite": "^1.0.30001809", "caniuse-lite": "^1.0.30001809",
@@ -1970,6 +2491,15 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/combined-stream": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -2008,7 +2538,6 @@
"version": "3.2.3", "version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/debug": { "node_modules/debug": {
@@ -2028,6 +2557,13 @@
} }
} }
}, },
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT",
"peer": true
},
"node_modules/deep-is": { "node_modules/deep-is": {
"version": "0.1.4", "version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -2053,6 +2589,16 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/dom-helpers": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.8.7",
"csstype": "^3.0.2"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2161,7 +2707,6 @@
"integrity": "sha512-5KeEOJZBfEVA47boFiBsf+6MmmJpffM7qEBg4pLla2e4nlKgdKlqCW0oSLOGsT8Wl5uCGJptLV1bkaiShj90Gw==", "integrity": "sha512-5KeEOJZBfEVA47boFiBsf+6MmmJpffM7qEBg4pLla2e4nlKgdKlqCW0oSLOGsT8Wl5uCGJptLV1bkaiShj90Gw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
], ],
@@ -2656,6 +3201,29 @@
"node": ">=0.8.19" "node": ">=0.8.19"
} }
}, },
"node_modules/input-otp": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz",
"integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/intl-messageformat": {
"version": "10.7.18",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz",
"integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.6",
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/icu-messageformat-parser": "2.11.4",
"tslib": "^2.8.0"
}
},
"node_modules/is-extglob": { "node_modules/is-extglob": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2699,7 +3267,6 @@
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/jsesc": { "node_modules/jsesc": {
@@ -3038,6 +3605,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": { "node_modules/lru-cache": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -3225,6 +3804,15 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/optionator": { "node_modules/optionator": {
"version": "0.9.4", "version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -3307,7 +3895,6 @@
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -3353,6 +3940,17 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/proxy-from-env": { "node_modules/proxy-from-env": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
@@ -3377,17 +3975,55 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-aria": {
"version": "3.52.1",
"resolved": "https://registry.npmjs.org/react-aria/-/react-aria-3.52.1.tgz",
"integrity": "sha512-fdZZruC9/x/joCg0mhKGs5aHpwrXLCSZ4GOJmhhYyiE0ffyEsk9MLFt9LCOCF9tn8ErTD+UDQP4oDUSlZkmpCg==",
"license": "Apache-2.0",
"dependencies": {
"@internationalized/date": "^3.12.4",
"@internationalized/number": "^3.6.8",
"@internationalized/string": "^3.2.10",
"@react-types/shared": "^3.36.1",
"@swc/helpers": "^0.5.0",
"aria-hidden": "^1.2.3",
"clsx": "^2.0.0",
"react-stately": "3.50.0",
"use-sync-external-store": "^1.6.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/react-aria-components": {
"version": "1.21.1",
"resolved": "https://registry.npmjs.org/react-aria-components/-/react-aria-components-1.21.1.tgz",
"integrity": "sha512-J88WflYY0z+EhLX0ce+GjFlQ3ag3ubIgfUTjpKSrBQBu92Xtl4Ub9o118HItUddtbk1Ido0jzyPy94/klZy1hg==",
"license": "Apache-2.0",
"dependencies": {
"@internationalized/date": "^3.12.4",
"@internationalized/string": "^3.2.10",
"@react-types/shared": "^3.36.1",
"@swc/helpers": "^0.5.0",
"client-only": "^0.0.1",
"react-aria": "3.52.1",
"react-stately": "3.50.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1",
"react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "19.2.8", "version": "19.2.8",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@@ -3404,6 +4040,45 @@
"react": "*" "react": "*"
} }
}, },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-stately": {
"version": "3.50.0",
"resolved": "https://registry.npmjs.org/react-stately/-/react-stately-3.50.0.tgz",
"integrity": "sha512-TnckvpDQGU0672wEaLZqdzvhuSeXWjgW6vijMWxiKOxc8CosQUfPGISdcZyfHqjX3XMjEtRxj4w9HumvX4h4mw==",
"license": "Apache-2.0",
"dependencies": {
"@internationalized/date": "^3.12.4",
"@internationalized/number": "^3.6.8",
"@internationalized/string": "^3.2.10",
"@react-types/shared": "^3.36.1",
"@swc/helpers": "^0.5.0",
"use-sync-external-store": "^1.6.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
},
"node_modules/react-transition-group": {
"version": "4.4.5",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
"license": "BSD-3-Clause",
"dependencies": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
},
"peerDependencies": {
"react": ">=16.6.0",
"react-dom": ">=16.6.0"
}
},
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.27.0", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -3554,6 +4229,38 @@
} }
} }
}, },
"node_modules/tailwind-merge": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
"integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
}
},
"node_modules/tailwind-variants": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.3.1.tgz",
"integrity": "sha512-4pAvwUtM4HKBiRZftncAbpn6V9Hhwoa5Fl7O2u5zbp7Z5Cvu+/o/6+176WY3WCEES209543quG8zFIcXCsc5Jw==",
"license": "MIT",
"engines": {
"node": ">=16.9.x",
"pnpm": ">=7.x"
},
"peerDependencies": {
"tailwind-merge": ">=3.0.0",
"tailwindcss": "*"
},
"peerDependenciesMeta": {
"tailwind-merge": {
"optional": true
},
"tailwindcss": {
"optional": true
}
}
},
"node_modules/tailwindcss": { "node_modules/tailwindcss": {
"version": "4.3.3", "version": "4.3.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
@@ -3609,6 +4316,15 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/tw-animate-css": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz",
"integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Wombosvideo"
}
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -3628,7 +4344,6 @@
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -3709,6 +4424,15 @@
"punycode": "^2.1.0" "punycode": "^2.1.0"
} }
}, },
"node_modules/use-sync-external-store": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.7.0.tgz",
"integrity": "sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -3761,7 +4485,6 @@
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
+2
View File
@@ -11,6 +11,8 @@
}, },
"dependencies": { "dependencies": {
"@azure/msal-browser": "^5.21.0", "@azure/msal-browser": "^5.21.0",
"@heroui/react": "^3.2.4",
"@heroui/styles": "^3.2.4",
"@react-oauth/google": "^0.13.5", "@react-oauth/google": "^0.13.5",
"@tailwindcss/postcss": "^4.3.3", "@tailwindcss/postcss": "^4.3.3",
"@tanstack/react-query": "^5.101.4", "@tanstack/react-query": "^5.101.4",
+6 -6
View File
@@ -163,7 +163,7 @@ export default function ComponentsContent() {
<Badge variant="danger" icon={<FiAlertCircle />}> <Badge variant="danger" icon={<FiAlertCircle />}>
Con ícono Con ícono
</Badge> </Badge>
<Badge variant="primary" removable onRemove={() => {}}> <Badge variant="primary" removable onRemove={() => { }}>
Removable Removable
</Badge> </Badge>
</Row> </Row>
@@ -279,15 +279,15 @@ export default function ComponentsContent() {
<Row> <Row>
<Checkbox <Checkbox
label="Recordar sesión" label="Recordar sesión"
checked={checked} isSelected={checked}
onChange={(e) => setChecked(e.target.checked)} onChange={(isSelected) => setChecked(isSelected)}
/> />
<Checkbox label="Deshabilitado" disabled /> <Checkbox label="Deshabilitado" disabled />
<Checkbox <Checkbox
label="Marcado fijo" label="Marcado fijo"
checked checked
disabled disabled
onChange={() => {}} onChange={() => { }}
/> />
</Row> </Row>
<Row> <Row>
@@ -302,8 +302,8 @@ export default function ComponentsContent() {
<CodeBlock <CodeBlock
code={`<Checkbox code={`<Checkbox
label="Recordar sesión" label="Recordar sesión"
checked={checked} isSelected={checked}
onChange={e => setChecked(e.target.checked)} onChange={(isSelected) => setChecked(isSelected)}
/> />
<Switch <Switch
+1
View File
@@ -1,4 +1,5 @@
@import "tailwindcss"; @import "tailwindcss";
@import "@heroui/styles";
@custom-variant dark (&:is(.dark, .dark *)); @custom-variant dark (&:is(.dark, .dark *));
+67 -68
View File
@@ -1,18 +1,49 @@
"use client"; "use client";
import React from 'react'; import React from 'react';
import { Chip as HeroUIChip, ChipProps as HeroUIChipProps } from '@heroui/react';
import { tv } from '@heroui/styles';
import { FiX } from 'react-icons/fi'; import { FiX } from 'react-icons/fi';
export interface BadgeProps { // 1. Organizamos los estilos con 'tv' usando variantes estándar
children: React.ReactNode; const badgeStyles = tv({
variant?: base: "inline-flex items-center select-none border transition-colors",
| 'default' variants: {
| 'primary' customVariant: {
| 'secondary' default: 'bg-slate-100 text-slate-800 border-slate-200 dark:bg-slate-800/80 dark:text-slate-200 dark:border-slate-700',
| 'success' primary: 'bg-cyan-50 text-cyan-700 border-cyan-200 dark:bg-cyan-950/60 dark:text-cyan-300 dark:border-cyan-500/30',
| 'warning' secondary: 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-950/60 dark:text-purple-300 dark:border-purple-500/30',
| 'danger' success: 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-950/60 dark:text-emerald-300 dark:border-emerald-500/30',
| 'info' warning: 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/60 dark:text-amber-300 dark:border-amber-500/30',
| 'outline'; danger: 'bg-rose-50 text-rose-700 border-rose-200 dark:bg-rose-950/60 dark:text-rose-300 dark:border-rose-500/30',
info: 'bg-sky-50 text-sky-700 border-sky-200 dark:bg-sky-950/60 dark:text-sky-300 dark:border-sky-500/30',
outline: 'bg-transparent text-slate-700 border-slate-300 dark:text-slate-300 dark:border-slate-700',
},
customSize: {
sm: 'text-[10px] px-2 py-0.5 gap-1 rounded-md font-semibold h-auto min-w-min',
md: 'text-xs px-2.5 py-1 gap-1.5 rounded-lg font-medium h-auto min-w-min',
lg: 'text-sm px-3 py-1.5 gap-2 rounded-xl font-medium h-auto min-w-min',
}
},
defaultVariants: {
customVariant: "default",
customSize: "md",
}
});
// Mapeo auxiliar de colores para los dots según la variante
const dotColorMap: Record<NonNullable<BadgeProps['variant']>, string> = {
default: 'bg-slate-500',
primary: 'bg-cyan-500',
secondary: 'bg-purple-500',
success: 'bg-emerald-500',
warning: 'bg-amber-500',
danger: 'bg-rose-500',
info: 'bg-sky-500',
outline: 'bg-slate-400',
};
export interface BadgeProps extends Omit<HeroUIChipProps, 'variant' | 'size' | 'color' | 'className' | 'children'> {
variant?: 'default' | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'outline';
size?: 'sm' | 'md' | 'lg'; size?: 'sm' | 'md' | 'lg';
dot?: boolean; dot?: boolean;
dotPulse?: boolean; dotPulse?: boolean;
@@ -20,50 +51,12 @@ export interface BadgeProps {
removable?: boolean; removable?: boolean;
onRemove?: () => void; onRemove?: () => void;
className?: string; className?: string;
children?: React.ReactNode;
} }
const variantStyles: Record<NonNullable<BadgeProps['variant']>, { badge: string; dot: string }> = { export const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
default: { (
badge: 'bg-slate-100 text-slate-800 border-slate-200 dark:bg-slate-800/80 dark:text-slate-200 dark:border-slate-700', {
dot: 'bg-slate-500',
},
primary: {
badge: 'bg-cyan-50 text-cyan-700 border-cyan-200 dark:bg-cyan-950/60 dark:text-cyan-300 dark:border-cyan-500/30',
dot: 'bg-cyan-500',
},
secondary: {
badge: 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-950/60 dark:text-purple-300 dark:border-purple-500/30',
dot: 'bg-purple-500',
},
success: {
badge: 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-950/60 dark:text-emerald-300 dark:border-emerald-500/30',
dot: 'bg-emerald-500',
},
warning: {
badge: 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/60 dark:text-amber-300 dark:border-amber-500/30',
dot: 'bg-amber-500',
},
danger: {
badge: 'bg-rose-50 text-rose-700 border-rose-200 dark:bg-rose-950/60 dark:text-rose-300 dark:border-rose-500/30',
dot: 'bg-rose-500',
},
info: {
badge: 'bg-sky-50 text-sky-700 border-sky-200 dark:bg-sky-950/60 dark:text-sky-300 dark:border-sky-500/30',
dot: 'bg-sky-500',
},
outline: {
badge: 'bg-transparent text-slate-700 border-slate-300 dark:text-slate-300 dark:border-slate-700',
dot: 'bg-slate-400',
},
};
const sizeStyles = {
sm: 'text-[10px] px-2 py-0.5 gap-1 rounded-md font-semibold',
md: 'text-xs px-2.5 py-1 gap-1.5 rounded-lg font-medium',
lg: 'text-sm px-3 py-1.5 gap-2 rounded-xl font-medium',
};
export const Badge: React.FC<BadgeProps> = ({
children, children,
variant = 'default', variant = 'default',
size = 'md', size = 'md',
@@ -73,27 +66,30 @@ export const Badge: React.FC<BadgeProps> = ({
removable = false, removable = false,
onRemove, onRemove,
className = '', className = '',
}) => { ...props
const currentVariant = variantStyles[variant]; },
ref
) => {
const dotColor = dotColorMap[variant];
return ( return (
<span <HeroUIChip
className={` ref={ref}
inline-flex items-center select-none border transition-colors // En HeroUI v3 se usa className en singular (soporta string directo desde tv)
${currentVariant.badge} className={badgeStyles({
${sizeStyles[size]} customVariant: variant,
${className} customSize: size,
`} className
})}
{...props}
> >
{/* Optional Dot indicator */} {/* Optional Dot indicator */}
{dot && ( {dot && (
<span className="relative flex h-1.5 w-1.5 shrink-0"> <span className="relative flex h-1.5 w-1.5 shrink-0">
{dotPulse && ( {dotPulse && (
<span <span className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${dotColor}`} />
className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${currentVariant.dot}`}
/>
)} )}
<span className={`relative inline-flex rounded-full h-1.5 w-1.5 ${currentVariant.dot}`} /> <span className={`relative inline-flex rounded-full h-1.5 w-1.5 ${dotColor}`} />
</span> </span>
)} )}
@@ -117,6 +113,9 @@ export const Badge: React.FC<BadgeProps> = ({
<FiX className="text-xs" /> <FiX className="text-xs" />
</button> </button>
)} )}
</span> </HeroUIChip>
); );
}; }
);
Badge.displayName = 'Badge';
+52 -44
View File
@@ -1,8 +1,43 @@
"use client"; "use client";
import React from 'react'; import React from 'react';
import { Button as HeroUIButton, ButtonProps as HeroUIButtonProps } from '@heroui/react';
import { tv } from '@heroui/styles';
import { Spinner } from './Spinner'; import { Spinner } from './Spinner';
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> { const customButtonStyles = tv({
base: "relative inline-flex items-center justify-center font-medium select-none transition-all duration-200 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950 active:scale-[0.98]",
variants: {
customVariant: {
primary: "bg-cyan-500 hover:bg-cyan-400 text-slate-950 font-semibold shadow-md shadow-cyan-500/20 hover:shadow-cyan-500/30 focus-visible:ring-cyan-500 dark:bg-cyan-500 dark:hover:bg-cyan-400 dark:text-slate-950 dark:shadow-cyan-500/30",
secondary: "bg-slate-100 hover:bg-slate-200 text-slate-800 border border-slate-200 shadow-sm focus-visible:ring-slate-400 dark:bg-slate-800 dark:hover:bg-slate-700 dark:text-slate-100 dark:border-slate-700 dark:shadow-none dark:focus-visible:ring-slate-500",
outline: "bg-transparent border border-slate-300 text-slate-700 hover:bg-slate-100 hover:border-slate-400 focus-visible:ring-slate-400 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800/80 dark:hover:border-slate-600 dark:focus-visible:ring-slate-500",
ghost: "bg-transparent text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:ring-slate-400 dark:text-slate-300 dark:hover:bg-slate-800/70 dark:hover:text-white dark:focus-visible:ring-slate-600",
danger: "bg-rose-600 hover:bg-rose-500 text-white font-medium shadow-md shadow-rose-600/25 focus-visible:ring-rose-500 dark:bg-rose-600 dark:hover:bg-rose-500 dark:text-white",
success: "bg-emerald-600 hover:bg-emerald-500 text-white font-medium shadow-md shadow-emerald-600/25 focus-visible:ring-emerald-500 dark:bg-emerald-500 dark:hover:bg-emerald-400 dark:text-slate-950",
gradient: "bg-gradient-to-r from-cyan-500 via-sky-500 to-blue-600 hover:from-cyan-400 hover:via-sky-400 hover:to-blue-500 text-white font-semibold shadow-lg shadow-cyan-500/25 hover:shadow-cyan-500/40 focus-visible:ring-cyan-500 dark:from-cyan-500 dark:to-blue-600",
violet: "bg-violet-600 hover:bg-violet-500 active:bg-violet-700 text-white font-bold shadow-lg shadow-violet-600/25 hover:shadow-violet-500/30 hover:-translate-y-0.5 focus-visible:ring-violet-500",
},
customSize: {
xs: "text-[11px] px-2.5 py-1 rounded-lg gap-1.5 h-auto min-w-min",
sm: "text-xs px-3 py-1.5 rounded-xl gap-2 h-auto min-w-min",
md: "text-sm px-4 py-2.5 rounded-xl gap-2 h-auto min-w-min",
lg: "text-base px-6 py-3.5 rounded-2xl gap-2.5 h-auto min-w-min",
icon: "p-2.5 rounded-xl justify-center items-center h-auto min-w-min",
},
fullWidth: {
true: "w-full",
},
isDisabled: {
true: "opacity-50 cursor-not-allowed pointer-events-none active:scale-100",
}
},
defaultVariants: {
customVariant: "primary",
customSize: "md",
}
});
export interface ButtonProps extends Omit<HeroUIButtonProps, 'variant' | 'size'> {
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'success' | 'gradient' | 'violet'; variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'success' | 'gradient' | 'violet';
size?: 'xs' | 'sm' | 'md' | 'lg' | 'icon'; size?: 'xs' | 'sm' | 'md' | 'lg' | 'icon';
isLoading?: boolean; isLoading?: boolean;
@@ -10,35 +45,11 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
leftIcon?: React.ReactNode; leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode; rightIcon?: React.ReactNode;
fullWidth?: boolean; fullWidth?: boolean;
disabled?: boolean;
className?: string;
children?: React.ReactNode;
} }
const variantStyles: Record<NonNullable<ButtonProps['variant']>, string> = {
primary:
'bg-cyan-500 hover:bg-cyan-400 text-slate-950 font-semibold shadow-md shadow-cyan-500/20 hover:shadow-cyan-500/30 focus-visible:ring-cyan-500 dark:bg-cyan-500 dark:hover:bg-cyan-400 dark:text-slate-950 dark:shadow-cyan-500/30',
secondary:
'bg-slate-100 hover:bg-slate-200 text-slate-800 border border-slate-200 shadow-sm focus-visible:ring-slate-400 dark:bg-slate-800 dark:hover:bg-slate-700 dark:text-slate-100 dark:border-slate-700 dark:shadow-none dark:focus-visible:ring-slate-500',
outline:
'bg-transparent border border-slate-300 text-slate-700 hover:bg-slate-100 hover:border-slate-400 focus-visible:ring-slate-400 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800/80 dark:hover:border-slate-600 dark:focus-visible:ring-slate-500',
ghost:
'bg-transparent text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus-visible:ring-slate-400 dark:text-slate-300 dark:hover:bg-slate-800/70 dark:hover:text-white dark:focus-visible:ring-slate-600',
danger:
'bg-rose-600 hover:bg-rose-500 text-white font-medium shadow-md shadow-rose-600/25 focus-visible:ring-rose-500 dark:bg-rose-600 dark:hover:bg-rose-500 dark:text-white',
success:
'bg-emerald-600 hover:bg-emerald-500 text-white font-medium shadow-md shadow-emerald-600/25 focus-visible:ring-emerald-500 dark:bg-emerald-500 dark:hover:bg-emerald-400 dark:text-slate-950',
gradient:
'bg-gradient-to-r from-cyan-500 via-sky-500 to-blue-600 hover:from-cyan-400 hover:via-sky-400 hover:to-blue-500 text-white font-semibold shadow-lg shadow-cyan-500/25 hover:shadow-cyan-500/40 focus-visible:ring-cyan-500 dark:from-cyan-500 dark:to-blue-600',
violet:
'bg-violet-600 hover:bg-violet-500 active:bg-violet-700 text-white font-bold shadow-lg shadow-violet-600/25 hover:shadow-violet-500/30 hover:-translate-y-0.5 focus-visible:ring-violet-500',
};
const sizeStyles: Record<NonNullable<ButtonProps['size']>, string> = {
xs: 'text-[11px] px-2.5 py-1 rounded-lg gap-1.5',
sm: 'text-xs px-3 py-1.5 rounded-xl gap-2',
md: 'text-sm px-4 py-2.5 rounded-xl gap-2',
lg: 'text-base px-6 py-3.5 rounded-2xl gap-2.5',
icon: 'p-2.5 rounded-xl justify-center items-center',
};
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
( (
{ {
@@ -50,30 +61,27 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
rightIcon, rightIcon,
fullWidth = false, fullWidth = false,
disabled, disabled,
isDisabled,
className = '', className = '',
children, children,
...props ...props
}, },
ref ref
) => { ) => {
const isInteractiveDisabled = disabled || isLoading; const actuallyDisabled = disabled || isDisabled || isLoading;
return ( return (
<button <HeroUIButton
ref={ref} ref={ref}
disabled={isInteractiveDisabled} isDisabled={actuallyDisabled}
className={` className={customButtonStyles({
relative inline-flex items-center justify-center font-medium select-none customVariant: variant,
transition-all duration-200 ease-out customSize: size,
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 fullWidth,
focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950 isDisabled: actuallyDisabled,
active:scale-[0.98] className
disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none disabled:active:scale-100 })}
${fullWidth ? 'w-full' : ''} variant={undefined}
${variantStyles[variant]}
${sizeStyles[size]}
${className}
`}
{...props} {...props}
> >
{isLoading ? ( {isLoading ? (
@@ -91,7 +99,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
{rightIcon && <span className="shrink-0">{rightIcon}</span>} {rightIcon && <span className="shrink-0">{rightIcon}</span>}
</> </>
)} )}
</button> </HeroUIButton>
); );
} }
); );
+126 -76
View File
@@ -1,147 +1,197 @@
"use client"; "use client";
import React from 'react';
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> { import React from "react";
variant?: 'default' | 'elevated' | 'glass' | 'outline' | 'interactive'; import { Card as HeroUICard } from "@heroui/react";
padding?: 'none' | 'sm' | 'md' | 'lg';
export type CardVariant =
| "default"
| "elevated"
| "glass"
| "outline"
| "interactive";
export type CardPadding = "none" | "sm" | "md" | "lg";
export interface CardProps
extends Omit<
React.ComponentProps<typeof HeroUICard>,
"variant" | "className"
> {
variant?: CardVariant;
padding?: CardPadding;
className?: string;
} }
const variantStyles: Record<NonNullable<CardProps['variant']>, string> = { const variantStyles: Record<CardVariant, string> = {
default: default:
'bg-white dark:bg-slate-900/90 border border-slate-200 dark:border-slate-800/80 shadow-sm', "bg-white dark:bg-slate-900/90 border border-slate-200 dark:border-slate-800/80 shadow-sm",
elevated: elevated:
'bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 shadow-xl shadow-slate-200/50 dark:shadow-black/40', "bg-white dark:bg-slate-900 border border-slate-100 dark:border-slate-800 shadow-xl shadow-slate-200/50 dark:shadow-black/40",
glass: glass:
'bg-white/80 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/70 dark:border-slate-800/70 shadow-lg', "bg-white/80 dark:bg-slate-900/60 backdrop-blur-xl border border-slate-200/70 dark:border-slate-800/70 shadow-lg",
outline: outline:
'bg-transparent border border-slate-300 dark:border-slate-800 shadow-none', "bg-transparent border border-slate-300 dark:border-slate-800 shadow-none",
interactive: interactive:
'bg-white dark:bg-slate-900/90 border border-slate-200 dark:border-slate-800 shadow-sm hover:shadow-xl hover:shadow-cyan-500/10 hover:border-cyan-500/50 dark:hover:border-cyan-500/40 cursor-pointer transform hover:-translate-y-1 transition-all duration-300 ease-out', "bg-white dark:bg-slate-900/90 border border-slate-200 dark:border-slate-800 shadow-sm " +
"hover:shadow-xl hover:shadow-cyan-500/10 " +
"hover:border-cyan-500/50 dark:hover:border-cyan-500/40 " +
"cursor-pointer transform hover:-translate-y-1 " +
"transition-all duration-300 ease-out",
}; };
const paddingStyles: Record<NonNullable<CardProps['padding']>, string> = { const paddingStyles: Record<CardPadding, string> = {
none: 'p-0', none: "p-0",
sm: 'p-4', sm: "p-4",
md: 'p-6', md: "p-6",
lg: 'p-8', lg: "p-8",
}; };
export const Card = React.forwardRef<HTMLDivElement, CardProps>( export const Card = ({
( variant = "default",
{ padding = "none",
variant = 'default', className = "",
padding = 'none',
className = '',
children, children,
...props ...props
}, }: CardProps) => {
ref
) => {
return ( return (
<div <HeroUICard
ref={ref}
className={`
rounded-2xl overflow-hidden transition-colors
${variantStyles[variant]}
${paddingStyles[padding]}
${className}
`}
{...props} {...props}
className={[
"rounded-2xl overflow-hidden transition-colors",
variantStyles[variant],
paddingStyles[padding],
className,
]
.filter(Boolean)
.join(" ")}
> >
{children} {children}
</div> </HeroUICard>
); );
} };
);
Card.displayName = 'Card'; export interface CardHeaderProps
extends React.ComponentProps<typeof HeroUICard.Header> {
export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
bordered?: boolean; bordered?: boolean;
} }
export const CardHeader: React.FC<CardHeaderProps> = ({ export const CardHeader = ({
bordered = false, bordered = false,
className = '', className = "",
children, children,
...props ...props
}) => { }: CardHeaderProps) => {
return ( return (
<div <HeroUICard.Header
className={`
p-5 sm:p-6 flex flex-col space-y-1.5
${bordered ? 'border-b border-slate-100 dark:border-slate-800/80' : ''}
${className}
`}
{...props} {...props}
className={[
"p-5 sm:p-6 flex flex-col space-y-1.5",
bordered
? "border-b border-slate-100 dark:border-slate-800/80"
: "",
className,
]
.filter(Boolean)
.join(" ")}
> >
{children} {children}
</div> </HeroUICard.Header>
); );
}; };
export const CardTitle: React.FC<React.HTMLAttributes<HTMLHeadingElement>> = ({ export const CardTitle = ({
className = '', className = "",
children, children,
...props ...props
}) => { }: React.ComponentProps<typeof HeroUICard.Title>) => {
return ( return (
<h3 <HeroUICard.Title
className={`text-lg sm:text-xl font-bold tracking-tight text-slate-900 dark:text-white transition-colors ${className}`}
{...props} {...props}
className={[
"text-lg sm:text-xl font-bold tracking-tight",
"text-slate-900 dark:text-white",
"transition-colors",
className,
]
.filter(Boolean)
.join(" ")}
> >
{children} {children}
</h3> </HeroUICard.Title>
); );
}; };
export const CardDescription: React.FC<React.HTMLAttributes<HTMLParagraphElement>> = ({ export const CardDescription = ({
className = '', className = "",
children, children,
...props ...props
}) => { }: React.ComponentProps<typeof HeroUICard.Description>) => {
return ( return (
<p <HeroUICard.Description
className={`text-xs sm:text-sm text-slate-500 dark:text-slate-400 leading-relaxed ${className}`}
{...props} {...props}
className={[
"text-xs sm:text-sm",
"text-slate-500 dark:text-slate-400",
"leading-relaxed",
className,
]
.filter(Boolean)
.join(" ")}
> >
{children} {children}
</p> </HeroUICard.Description>
); );
}; };
export const CardContent: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ export const CardContent = ({
className = '', className = "",
children, children,
...props ...props
}) => { }: React.ComponentProps<typeof HeroUICard.Content>) => {
return ( return (
<div className={`p-5 sm:p-6 ${className}`} {...props}> <HeroUICard.Content
{...props}
className={[
"p-5 sm:p-6",
className,
]
.filter(Boolean)
.join(" ")}
>
{children} {children}
</div> </HeroUICard.Content>
); );
}; };
export interface CardFooterProps extends React.HTMLAttributes<HTMLDivElement> { export interface CardFooterProps
extends React.ComponentProps<typeof HeroUICard.Footer> {
bordered?: boolean; bordered?: boolean;
} }
export const CardFooter: React.FC<CardFooterProps> = ({ export const CardFooter = ({
bordered = false, bordered = false,
className = '', className = "",
children, children,
...props ...props
}) => { }: CardFooterProps) => {
return ( return (
<div <HeroUICard.Footer
className={`
p-5 sm:p-6 flex items-center justify-between
${bordered ? 'border-t border-slate-100 dark:border-slate-800/80' : ''}
${className}
`}
{...props} {...props}
className={[
"p-5 sm:p-6 flex items-center justify-between",
bordered
? "border-t border-slate-100 dark:border-slate-800/80"
: "",
className,
]
.filter(Boolean)
.join(" ")}
> >
{children} {children}
</div> </HeroUICard.Footer>
); );
}; };
+112 -76
View File
@@ -1,112 +1,148 @@
"use client"; "use client";
import React, { useEffect, useRef } from 'react';
import { FiCheck, FiMinus } from 'react-icons/fi';
export interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> { import React from "react";
import {
Checkbox as HeroUICheckbox,
Description,
FieldError,
} from "@heroui/react";
import { FiCheck, FiMinus } from "react-icons/fi";
export interface CheckboxProps
extends Omit<
React.ComponentProps<typeof HeroUICheckbox>,
"children" | "defaultSelected" | "isDisabled" | "isInvalid" | "isIndeterminate"
> {
label?: React.ReactNode; label?: React.ReactNode;
description?: string; description?: string;
errorMessage?: string; errorMessage?: string;
checked?: boolean;
disabled?: boolean;
indeterminate?: boolean; indeterminate?: boolean;
className?: string;
} }
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>( export const Checkbox = ({
(
{
label, label,
description, description,
errorMessage, errorMessage,
indeterminate = false, indeterminate = false,
checked, checked,
disabled, isSelected,
className = '', disabled = false,
className = "",
id, id,
name, name,
onChange,
...props ...props
}, }: CheckboxProps) => {
forwardedRef
) => {
const internalRef = useRef<HTMLInputElement>(null);
const resolvedRef = (forwardedRef as React.RefObject<HTMLInputElement>) || internalRef;
const checkboxId = id || name || (typeof label === 'string' ? label.toLowerCase().replace(/\s+/g, '-') : undefined);
useEffect(() => {
if (resolvedRef.current) {
resolvedRef.current.indeterminate = indeterminate;
}
}, [indeterminate, resolvedRef]);
const isChecked = Boolean(checked);
return ( return (
<div className={`flex flex-col space-y-1 ${className}`}> <HeroUICheckbox
<label
htmlFor={checkboxId}
className={`inline-flex items-start gap-2.5 select-none cursor-pointer group ${
disabled ? 'opacity-50 !cursor-not-allowed' : ''
}`}
>
{/* Custom Styled Box */}
<div className="relative flex items-center justify-center mt-0.5 shrink-0">
<input
ref={resolvedRef}
id={checkboxId}
name={name}
type="checkbox"
checked={checked}
disabled={disabled}
className="peer sr-only"
{...props} {...props}
/> id={id}
name={name}
<div isSelected={checked}
className={` isDisabled={disabled}
w-4 h-4 rounded-md flex items-center justify-center transition-all duration-200 isIndeterminate={indeterminate}
border isInvalid={Boolean(errorMessage)}
peer-focus-visible:ring-2 peer-focus-visible:ring-offset-2 onChange={onChange}
peer-focus-visible:ring-cyan-500 peer-focus-visible:ring-offset-white dark:peer-focus-visible:ring-offset-slate-950 className={className}
${
isChecked || indeterminate
? 'bg-cyan-500 border-cyan-500 text-slate-950 shadow-sm shadow-cyan-500/20'
: 'bg-white dark:bg-slate-900 border-slate-300 dark:border-slate-700 text-transparent group-hover:border-slate-400 dark:group-hover:border-slate-500'
}
${
errorMessage
? '!border-rose-500 !bg-rose-50 dark:!bg-rose-950/30'
: ''
}
`}
> >
{indeterminate ? ( <HeroUICheckbox.Content className="items-start gap-2.5">
<FiMinus className="text-xs stroke-3" /> <HeroUICheckbox.Control
) : isChecked ? ( className={[
<FiCheck className="text-xs stroke-3" /> "mt-0.5 shrink-0 size-4 rounded-md",
) : null} "border",
</div> "transition-all duration-200",
</div>
// Estado normal
"bg-white dark:bg-slate-900",
"border-slate-300 dark:border-slate-700",
// Hover
"group-hover:border-slate-400",
"dark:group-hover:border-slate-500",
// Focus
"data-[focus-visible=true]:ring-2",
"data-[focus-visible=true]:ring-cyan-500",
"data-[focus-visible=true]:ring-offset-2",
"data-[focus-visible=true]:ring-offset-white",
"dark:data-[focus-visible=true]:ring-offset-slate-950",
// Seleccionado
"data-[selected=true]:bg-cyan-500",
"data-[selected=true]:border-cyan-500",
"data-[selected=true]:text-slate-950",
"data-[selected=true]:shadow-sm",
"data-[selected=true]:shadow-cyan-500/20",
// Indeterminado
"data-[indeterminate=true]:bg-cyan-500",
"data-[indeterminate=true]:border-cyan-500",
"data-[indeterminate=true]:text-slate-950",
// Error
errorMessage
? "data-[invalid=true]:border-rose-500"
: "",
]
.filter(Boolean)
.join(" ")}
>
<HeroUICheckbox.Indicator>
{({ isSelected, isIndeterminate }) => {
if (isIndeterminate) {
return <FiMinus className="text-xs stroke-3" />;
}
if (isSelected) {
return <FiCheck className="text-xs stroke-3" />;
}
return null;
}}
</HeroUICheckbox.Indicator>
</HeroUICheckbox.Control>
{/* Label and Sub-description */}
{(label || description) && ( {(label || description) && (
<div className="flex flex-col text-left"> <div className="flex flex-col text-left">
{label && ( {label && (
<span className="text-xs sm:text-sm font-medium text-slate-800 dark:text-slate-200 group-hover:text-slate-950 dark:group-hover:text-white transition-colors"> <span
className="
text-xs sm:text-sm font-medium
text-slate-800 dark:text-slate-200
group-hover:text-slate-950
dark:group-hover:text-white
transition-colors
"
>
{label} {label}
</span> </span>
)} )}
{description && ( {description && (
<span className="text-xs text-slate-500 dark:text-slate-400"> <Description
className="
text-xs
text-slate-500 dark:text-slate-400
"
>
{description} {description}
</span> </Description>
)} )}
</div> </div>
)} )}
</label> </HeroUICheckbox.Content>
{errorMessage && ( {errorMessage && (
<p className="text-xs text-rose-600 dark:text-rose-400 pl-6.5">{errorMessage}</p> <FieldError className="pl-6.5 text-xs">
{errorMessage}
</FieldError>
)} )}
</div> </HeroUICheckbox>
); );
} };
);
Checkbox.displayName = 'Checkbox'; Checkbox.displayName = "Checkbox";
+317 -251
View File
@@ -1,289 +1,355 @@
"use client"; "use client";
import { useState, useRef, useEffect, useCallback } from "react";
import { createPortal } from "react-dom";
import { FiCalendar, FiChevronLeft, FiChevronRight, FiX } from "react-icons/fi";
interface DatePickerProps { import React, { useMemo } from "react";
import {
DatePicker as HeroUIDatePicker,
DateField as HeroUIDateField,
Calendar as HeroUICalendar,
Label,
FieldError,
} from "@heroui/react";
import type { DateValue } from "@internationalized/date";
import { parseDate as parseInternationalDate } from "@internationalized/date";
import {
FiCalendar,
FiChevronLeft,
FiChevronRight,
FiX,
} from "react-icons/fi";
export interface DatePickerProps {
label?: string; label?: string;
value?: string; // "YYYY-MM-DD" value?: string; //Fecha en formato YYYY-MM-DD o undefined cuando se limpia
onChange: (value: string | undefined) => void; onChange: (value: string | undefined) => void; //Devuelve la fecha en formato YYYY-MM-DD o undefined cuando se limpia.
placeholder?: string; placeholder?: string;
required?: boolean; required?: boolean;
maxDate?: Date; maxDate?: Date; //Fecha máxima permitida.
className?: string;
errorMessage?: string;
} }
const MONTHS = [ function toCalendarDate(dateStr?: string): DateValue | undefined {
"Enero","Febrero","Marzo","Abril","Mayo","Junio", if (!dateStr) {
"Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre", return undefined;
]; }
const DAYS = ["Do","Lu","Ma","Mi","Ju","Vi","Sa"];
const DROPDOWN_H = 320; // px estimado del dropdown
function parseDate(value?: string): Date | undefined { try {
if (!value) return undefined; return parseInternationalDate(dateStr);
const d = new Date(value + "T12:00:00"); } catch {
return isNaN(d.getTime()) ? undefined : d; return undefined;
}
} }
function formatDisplay(value?: string): string { function toDateString(date: DateValue): string {
const d = parseDate(value); const yyyy = date.year;
if (!d) return ""; const mm = String(date.month).padStart(2, "0");
return d.toLocaleDateString("es-MX", { day: "numeric", month: "long", year: "numeric" }); const dd = String(date.day).padStart(2, "0");
return `${yyyy}-${mm}-${dd}`;
} }
export function DatePicker({ export const DatePicker = React.forwardRef<
HTMLDivElement,
DatePickerProps
>(
(
{
label, label,
value, value,
onChange, onChange,
placeholder = "Selecciona una fecha", placeholder,
required, required = false,
maxDate = new Date(), maxDate,
}: DatePickerProps) { className = "",
const selected = parseDate(value); errorMessage,
const [open, setOpen] = useState(false); },
const [view, setView] = useState<"days" | "months" | "years">("days"); ref
const [cursor, setCursor] = useState(() => { ) => {
const d = selected ?? new Date(); const dateValue = useMemo(
return { month: d.getMonth(), year: d.getFullYear() }; () => toCalendarDate(value),
}); [value]
const [pos, setPos] = useState({ top: 0, left: 0, width: 0, openUp: false }); );
const triggerRef = useRef<HTMLButtonElement>(null); const maxDateValue = useMemo<DateValue | undefined>(() => {
const dropdownRef = useRef<HTMLDivElement>(null); if (!maxDate) {
return undefined;
}
const calcPos = useCallback(() => { try {
if (!triggerRef.current) return; const yyyy = maxDate.getFullYear();
const r = triggerRef.current.getBoundingClientRect(); const mm = String(maxDate.getMonth() + 1).padStart(2, "0");
const spaceBelow = window.innerHeight - r.bottom; const dd = String(maxDate.getDate()).padStart(2, "0");
const openUp = spaceBelow < DROPDOWN_H + 8 && r.top > DROPDOWN_H + 8;
setPos({
top: openUp ? r.top - 8 : r.bottom + 8,
left: r.left,
width: r.width,
openUp,
});
}, []);
const openPicker = () => { return parseInternationalDate(`${yyyy}-${mm}-${dd}`);
if (selected) setCursor({ month: selected.getMonth(), year: selected.getFullYear() }); } catch {
setView("days"); return undefined;
calcPos(); }
setOpen(true); }, [maxDate]);
const handleChange = (newDate: DateValue | null) => {
if (newDate) {
onChange(toDateString(newDate));
} else {
onChange(undefined);
}
}; };
useEffect(() => { const handleClear = () => {
if (!open) return; onChange(undefined);
const onScroll = () => calcPos();
const onResize = () => calcPos();
const onMouse = (e: MouseEvent) => {
if (
triggerRef.current?.contains(e.target as Node) ||
dropdownRef.current?.contains(e.target as Node)
) return;
setOpen(false);
}; };
window.addEventListener("scroll", onScroll, true);
window.addEventListener("resize", onResize);
document.addEventListener("mousedown", onMouse);
return () => {
window.removeEventListener("scroll", onScroll, true);
window.removeEventListener("resize", onResize);
document.removeEventListener("mousedown", onMouse);
};
}, [open, calcPos]);
const selectDay = (day: number) => {
const d = new Date(cursor.year, cursor.month, day);
const iso = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
onChange(iso);
setOpen(false);
};
const daysInMonth = new Date(cursor.year, cursor.month + 1, 0).getDate();
const firstDow = new Date(cursor.year, cursor.month, 1).getDay();
const today = new Date();
const isDisabled = (day: number) => new Date(cursor.year, cursor.month, day) > maxDate;
const isSelected = (day: number) =>
selected?.getFullYear() === cursor.year &&
selected?.getMonth() === cursor.month &&
selected?.getDate() === day;
const isToday = (day: number) =>
today.getFullYear() === cursor.year &&
today.getMonth() === cursor.month &&
today.getDate() === day;
const yearRange = Array.from({ length: 101 }, (_, i) => today.getFullYear() - 100 + i).reverse();
const dropdown = open ? (
<div
ref={dropdownRef}
style={{
position: "fixed",
top: pos.openUp ? "auto" : pos.top,
bottom: pos.openUp ? window.innerHeight - pos.top : "auto",
left: pos.left,
width: Math.max(pos.width, 288),
zIndex: 9999,
}}
className="rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 overflow-hidden"
>
{/* Header */}
<div className="flex items-center justify-between px-4 py-3 border-b border-slate-100 dark:border-slate-800">
<button
type="button"
onClick={() => view === "days" && setCursor(c => {
const d = new Date(c.year, c.month - 1, 1);
return { month: d.getMonth(), year: d.getFullYear() };
})}
className="p-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition text-slate-500 dark:text-slate-400"
>
<FiChevronLeft />
</button>
<div className="flex gap-2">
<button
type="button"
onClick={() => setView(v => v === "months" ? "days" : "months")}
className="text-sm font-semibold text-slate-800 dark:text-slate-100 hover:text-violet-600 dark:hover:text-violet-400 transition"
>
{MONTHS[cursor.month]}
</button>
<button
type="button"
onClick={() => setView(v => v === "years" ? "days" : "years")}
className="text-sm font-semibold text-slate-800 dark:text-slate-100 hover:text-violet-600 dark:hover:text-violet-400 transition"
>
{cursor.year}
</button>
</div>
<button
type="button"
onClick={() => view === "days" && setCursor(c => {
const d = new Date(c.year, c.month + 1, 1);
return { month: d.getMonth(), year: d.getFullYear() };
})}
className="p-1.5 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-800 transition text-slate-500 dark:text-slate-400"
>
<FiChevronRight />
</button>
</div>
{/* Days */}
{view === "days" && (
<div className="p-3">
<div className="grid grid-cols-7 mb-1">
{DAYS.map(d => (
<div key={d} className="text-center text-[10px] font-semibold text-slate-400 dark:text-slate-500 py-1">{d}</div>
))}
</div>
<div className="grid grid-cols-7 gap-y-0.5">
{Array.from({ length: firstDow }).map((_, i) => <div key={`e-${i}`} />)}
{Array.from({ length: daysInMonth }, (_, i) => i + 1).map(day => (
<button
key={day}
type="button"
disabled={isDisabled(day)}
onClick={() => selectDay(day)}
className={`
aspect-square w-full flex items-center justify-center rounded-lg text-sm transition-all
${isSelected(day)
? "bg-violet-600 text-white font-semibold"
: isToday(day)
? "border border-violet-400 text-violet-600 dark:text-violet-400 font-semibold"
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800"}
${isDisabled(day) ? "opacity-30 cursor-not-allowed" : ""}
`}
>
{day}
</button>
))}
</div>
</div>
)}
{/* Months */}
{view === "months" && (
<div className="grid grid-cols-3 gap-2 p-3">
{MONTHS.map((m, i) => (
<button
key={m}
type="button"
onClick={() => { setCursor(c => ({ ...c, month: i })); setView("days"); }}
className={`py-2 rounded-xl text-sm font-medium transition ${
cursor.month === i
? "bg-violet-600 text-white"
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800"
}`}
>
{m.slice(0, 3)}
</button>
))}
</div>
)}
{/* Years */}
{view === "years" && (
<div className="h-52 overflow-y-auto p-2 grid grid-cols-3 gap-1 content-start">
{yearRange.map(y => (
<button
key={y}
type="button"
onClick={() => { setCursor(c => ({ ...c, year: y })); setView("days"); }}
className={`py-2 rounded-xl text-sm font-medium transition ${
cursor.year === y
? "bg-violet-600 text-white"
: "text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800"
}`}
>
{y}
</button>
))}
</div>
)}
</div>
) : null;
return ( return (
<div className="w-full space-y-1.5"> <HeroUIDatePicker.Root
ref={ref}
value={dateValue ?? null}
onChange={handleChange}
maxValue={maxDateValue}
isRequired={required}
isInvalid={Boolean(errorMessage)}
className={`w-full flex flex-col gap-1.5 ${className}`}
>
{/* Label */}
{label && ( {label && (
<label className="block text-xs font-semibold text-slate-700 dark:text-slate-300"> <Label className="block text-xs font-semibold text-slate-700 dark:text-slate-300">
{label} {label}
{required && <span className="ml-1 text-rose-500 font-bold">*</span>}
</label> {required && (
<span className="ml-1 font-bold text-rose-500">
*
</span>
)}
</Label>
)} )}
<div className="relative flex items-center"> {/* Campo de fecha */}
<div className="absolute left-3.5 pointer-events-none text-slate-400 dark:text-slate-500"> <HeroUIDateField.Group
<FiCalendar className="text-base" /> className={[
</div> "bg-white dark:bg-slate-900/90",
<button "border border-slate-300 dark:border-slate-800",
ref={triggerRef} "hover:border-slate-400 dark:hover:border-slate-700",
type="button" "data-[focus-within=true]:border-cyan-500",
onClick={openPicker} "data-[focus-within=true]:ring-4",
className={` "data-[focus-within=true]:ring-cyan-500/15",
w-full pl-10 pr-10 py-2.5 text-sm rounded-xl text-left transition-all duration-200 outline-none "rounded-xl flex items-center shadow-sm",
bg-white dark:bg-slate-900/90 "transition-all duration-200 h-10",
border border-slate-300 dark:border-slate-800 errorMessage
hover:border-slate-400 dark:hover:border-slate-700 ? "border-rose-500 dark:border-rose-500"
focus:border-cyan-500 focus:ring-4 focus:ring-cyan-500/15 : "",
dark:focus:border-cyan-500 dark:focus:ring-cyan-500/25 ].join(" ")}
${value ? "text-slate-900 dark:text-slate-100" : "text-slate-400 dark:text-slate-500"}
`}
> >
{value ? formatDisplay(value) : placeholder} {/* Icono izquierdo */}
</button> <HeroUIDateField.Prefix
className="pl-3.5 pr-1.5 text-slate-400 dark:text-slate-500"
>
<FiCalendar className="text-base" />
</HeroUIDateField.Prefix>
{/* Input segmentado */}
<HeroUIDateField.Input
className="flex flex-1 items-center text-sm text-slate-900 outline-none dark:text-slate-100"
>
{(segment) => (
<HeroUIDateField.Segment
segment={segment}
className={[
"min-w-5 rounded px-0.5 text-center outline-none",
"data-[placeholder=true]:text-slate-400",
"dark:data-[placeholder=true]:text-slate-500",
"focus:bg-cyan-500/20",
"focus:text-cyan-700",
"dark:focus:text-cyan-400",
].join(" ")}
/>
)}
</HeroUIDateField.Input>
{!value && placeholder && (
<span className="pointer-events-none absolute ml-10 text-sm text-slate-400 dark:text-slate-500">
{placeholder}
</span>
)}
<HeroUIDateField.Suffix className="flex items-center">
{/* Botón limpiar */}
{value && ( {value && (
<button <button
type="button" type="button"
onClick={() => onChange(undefined)} onClick={handleClear}
className="absolute right-3 p-1 rounded-md text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 transition" aria-label="Limpiar fecha"
className={[
"mr-1 rounded-md p-1",
"text-slate-400",
"hover:bg-slate-100 hover:text-slate-700",
"dark:hover:bg-slate-800 dark:hover:text-slate-200",
"outline-none transition",
].join(" ")}
> >
<FiX className="text-sm" /> <FiX className="text-sm" />
</button> </button>
)} )}
</div>
{typeof window !== "undefined" && createPortal(dropdown, document.body)} {/* Botón calendario */}
<HeroUIDatePicker.Trigger
className={[
"rounded-r-xl pl-1.5 pr-3",
"text-slate-400",
"hover:text-slate-700",
"dark:hover:text-slate-200",
"outline-none",
"focus-visible:text-cyan-500",
].join(" ")}
>
<HeroUIDatePicker.TriggerIndicator />
</HeroUIDatePicker.Trigger>
</HeroUIDateField.Suffix>
</HeroUIDateField.Group>
{/* Error */}
{errorMessage && (
<FieldError className="text-xs text-rose-500">
{errorMessage}
</FieldError>
)}
{/* Calendario */}
<HeroUIDatePicker.Popover
className={[
"min-w-[288px]",
"overflow-hidden rounded-2xl",
"border border-slate-200 dark:border-slate-700",
"bg-white dark:bg-slate-900",
"shadow-2xl",
"outline-none",
].join(" ")}
>
<HeroUICalendar.Root aria-label={label ?? "Seleccionar fecha"}>
{/* Header */}
<HeroUICalendar.Header
className={[
"flex items-center justify-between",
"border-b border-slate-100 dark:border-slate-800",
"px-4 py-3",
].join(" ")}
>
{/* Mes anterior */}
<HeroUICalendar.NavButton
slot="previous"
className={[
"rounded-lg p-1.5",
"text-slate-500 dark:text-slate-400",
"hover:bg-slate-100 dark:hover:bg-slate-800",
"transition",
"outline-none",
"focus-visible:ring-2 focus-visible:ring-cyan-500",
].join(" ")}
>
<FiChevronLeft className="text-lg" />
</HeroUICalendar.NavButton>
{/* Mes / año */}
<HeroUICalendar.Heading
className={[
"text-sm font-semibold",
"text-slate-800 dark:text-slate-100",
].join(" ")}
/>
{/* Mes siguiente */}
<HeroUICalendar.NavButton
slot="next"
className={[
"rounded-lg p-1.5",
"text-slate-500 dark:text-slate-400",
"hover:bg-slate-100 dark:hover:bg-slate-800",
"transition",
"outline-none",
"focus-visible:ring-2 focus-visible:ring-cyan-500",
].join(" ")}
>
<FiChevronRight className="text-lg" />
</HeroUICalendar.NavButton>
</HeroUICalendar.Header>
{/* Grid del calendario */}
<div className="p-3">
<HeroUICalendar.Grid className="w-full border-collapse">
{/* Días de la semana */}
<HeroUICalendar.GridHeader>
{(day) => (
<HeroUICalendar.HeaderCell
className={[
"pb-2 text-center",
"text-[10px] font-semibold",
"text-slate-400 dark:text-slate-500",
].join(" ")}
>
{day}
</HeroUICalendar.HeaderCell>
)}
</HeroUICalendar.GridHeader>
{/* Días */}
<HeroUICalendar.GridBody>
{(date) => (
<HeroUICalendar.Cell
date={date}
className="p-0.5 outline-none"
>
{({
formattedDate,
isSelected,
isDisabled,
isToday,
}) => (
<HeroUICalendar.CellIndicator
className={[
"flex aspect-square w-full min-w-8",
"items-center justify-center",
"rounded-lg text-sm transition-all",
"outline-none",
"focus-visible:ring-2",
"focus-visible:ring-cyan-500",
"focus-visible:ring-offset-1",
"focus-visible:ring-offset-white",
"dark:focus-visible:ring-offset-slate-900",
isSelected
? "bg-violet-600 font-semibold text-white shadow-sm"
: isToday
? [
"border border-violet-400",
"font-semibold text-violet-600",
"hover:bg-slate-100",
"dark:text-violet-400",
"dark:hover:bg-slate-800",
].join(" ")
: [
"text-slate-700",
"hover:bg-slate-100",
"dark:text-slate-300",
"dark:hover:bg-slate-800",
].join(" "),
isDisabled
? "cursor-not-allowed opacity-30 hover:bg-transparent"
: "",
].join(" ")}
>
{formattedDate}
</HeroUICalendar.CellIndicator>
)}
</HeroUICalendar.Cell>
)}
</HeroUICalendar.GridBody>
</HeroUICalendar.Grid>
</div> </div>
</HeroUICalendar.Root>
</HeroUIDatePicker.Popover>
</HeroUIDatePicker.Root>
); );
} }
);
DatePicker.displayName = "DatePicker";
+88 -52
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import React, { useState, useRef, useEffect } from "react"; import React, { useState, useRef, useEffect } from "react";
import { FiCheck, FiChevronDown, FiSearch, FiX } from "react-icons/fi"; import { FiCheck, FiChevronDown, FiSearch, FiX } from "react-icons/fi";
import { Popover as HeroUIPopover } from "@heroui/react";
import { useTheme } from "../../context/ThemeContext"; import { useTheme } from "../../context/ThemeContext";
export interface DropdownOption { export interface DropdownOption {
@@ -27,7 +28,6 @@ export const Dropdown: React.FC<DropdownProps> = ({
const { isDark } = useTheme(); const { isDark } = useTheme();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const ref = useRef<HTMLDivElement>(null);
const searchRef = useRef<HTMLInputElement>(null); const searchRef = useRef<HTMLInputElement>(null);
const filtered = options.filter((o) => const filtered = options.filter((o) =>
@@ -35,24 +35,18 @@ export const Dropdown: React.FC<DropdownProps> = ({
); );
useEffect(() => { useEffect(() => {
const handler = (e: MouseEvent) => { if (!open) {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
setSearch(""); setSearch("");
return;
} }
}; const timer = setTimeout(() => searchRef.current?.focus(), 50);
document.addEventListener("mousedown", handler); return () => clearTimeout(timer);
return () => document.removeEventListener("mousedown", handler);
}, []);
useEffect(() => {
if (open) setTimeout(() => searchRef.current?.focus(), 50);
}, [open]); }, [open]);
const toggle = (val: string) => const toggle = (val: string) =>
onChange(value.includes(val) ? value.filter((v) => v !== val) : [...value, val]); onChange(value.includes(val) ? value.filter((v) => v !== val) : [...value, val]);
const removeTag = (val: string, e: React.MouseEvent) => { const removeTag = (val: string, e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation(); e.stopPropagation();
onChange(value.filter((v) => v !== val)); onChange(value.filter((v) => v !== val));
}; };
@@ -71,13 +65,13 @@ export const Dropdown: React.FC<DropdownProps> = ({
const divider = isDark ? "bg-slate-700" : "bg-slate-100"; const divider = isDark ? "bg-slate-700" : "bg-slate-100";
return ( return (
<div ref={ref} className={`relative ${className}`}> <div className={`relative ${className}`}>
<HeroUIPopover.Root isOpen={open} onOpenChange={setOpen}>
{/* Trigger */} <HeroUIPopover.Trigger className="block w-full">
<div <button
onClick={() => setOpen((p) => !p)} type="button"
className={`flex items-center gap-2 min-h-[46px] w-full px-4 py-2 cursor-pointer border transition-colors duration-150 ${bg} ${borderColor} ${hoverBorder} ${ className={`flex items-center gap-2 min-h-11.5 w-full px-4 py-2 cursor-pointer border transition-colors duration-150 outline-none ${bg} ${borderColor} ${hoverBorder} ${
open ? "rounded-t-xl border-b-0" : "rounded-xl" open ? "rounded-t-xl rounded-b-none! border-b-0!" : "rounded-xl"
}`} }`}
> >
<div className="flex-1 flex items-center gap-1.5 min-w-0 overflow-hidden"> <div className="flex-1 flex items-center gap-1.5 min-w-0 overflow-hidden">
@@ -88,35 +82,54 @@ export const Dropdown: React.FC<DropdownProps> = ({
) : ( ) : (
<> <>
{value.slice(0, 1).map((v) => ( {value.slice(0, 1).map((v) => (
<span key={v} className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-lg text-xs font-semibold shrink-0 ${ <span
key={v}
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-lg text-xs font-semibold shrink-0 ${
isDark ? "bg-violet-600/25 text-violet-300" : "bg-violet-100 text-violet-700" isDark ? "bg-violet-600/25 text-violet-300" : "bg-violet-100 text-violet-700"
}`}> }`}
>
{labelOf(v)} {labelOf(v)}
<button type="button" onClick={(e) => removeTag(v, e)} className="hover:opacity-70"> <div
onClick={(e) => removeTag(v, e as any)}
className="hover:opacity-70 ml-1 cursor-pointer"
>
<FiX className="text-[10px]" /> <FiX className="text-[10px]" />
</button> </div>
</span> </span>
))} ))}
{value.length > 1 && ( {value.length > 1 && (
<span className={`text-xs font-semibold px-2 py-0.5 rounded-lg shrink-0 ${ <span
className={`text-xs font-semibold px-2 py-0.5 rounded-lg shrink-0 ${
isDark ? "bg-slate-700 text-slate-400" : "bg-slate-100 text-slate-500" isDark ? "bg-slate-700 text-slate-400" : "bg-slate-100 text-slate-500"
}`}> }`}
+{value.length - 2} más >
+{value.length - 1} más
</span> </span>
)} )}
</> </>
)} )}
</div> </div>
<FiChevronDown className={`shrink-0 text-sm transition-transform duration-200 ${open ? "rotate-180" : ""} ${isDark ? "text-slate-500" : "text-slate-400"}`} /> <FiChevronDown
</div> className={`shrink-0 text-sm transition-transform duration-200 ${
open ? "rotate-180" : ""
} ${isDark ? "text-slate-500" : "text-slate-400"}`}
/>
</button>
</HeroUIPopover.Trigger>
{/* Menu — absolute, sin border-top para no doblar la línea */} <HeroUIPopover.Content
{open && ( offset={0}
<div className={`absolute left-0 top-full z-50 w-full rounded-b-xl border border-t-0 shadow-lg overflow-hidden ${bg} ${borderColor}`}> placement="bottom start"
className={`w-(--trigger-width) rounded-b-xl border shadow-lg overflow-hidden p-0 m-0 ${bg} ${borderColor} rounded-t-none! border-t-0!`}
{/* Search */} >
<HeroUIPopover.Dialog className="w-full flex flex-col p-0 outline-none">
<div className={`h-px ${divider}`} /> <div className={`h-px ${divider}`} />
<div className={`flex items-center gap-2 px-3 py-2 mx-3 my-2 rounded-lg border ${isDark ? "border-slate-700 bg-slate-900/50" : "border-slate-200 bg-slate-50"}`}>
<div
className={`flex items-center gap-2 px-3 py-2 mx-3 my-2 rounded-lg border ${
isDark ? "border-slate-700 bg-slate-900/50" : "border-slate-200 bg-slate-50"
}`}
>
<FiSearch className={`shrink-0 text-sm ${isDark ? "text-slate-500" : "text-slate-400"}`} /> <FiSearch className={`shrink-0 text-sm ${isDark ? "text-slate-500" : "text-slate-400"}`} />
<input <input
ref={searchRef} ref={searchRef}
@@ -129,7 +142,11 @@ export const Dropdown: React.FC<DropdownProps> = ({
}`} }`}
/> />
{search && ( {search && (
<button onClick={() => setSearch("")} className={isDark ? "text-slate-500 hover:text-slate-300" : "text-slate-400 hover:text-slate-600"}> <button
type="button"
onClick={() => setSearch("")}
className={isDark ? "text-slate-500 hover:text-slate-300" : "text-slate-400 hover:text-slate-600"}
>
<FiX className="text-xs" /> <FiX className="text-xs" />
</button> </button>
)} )}
@@ -137,8 +154,7 @@ export const Dropdown: React.FC<DropdownProps> = ({
<div className={`h-px ${divider}`} /> <div className={`h-px ${divider}`} />
{/* Options */} <div className="max-h-52 overflow-y-auto py-1 w-full">
<div className="max-h-52 overflow-y-auto py-1">
{filtered.length === 0 ? ( {filtered.length === 0 ? (
<p className={`px-4 py-3 text-xs text-center ${isDark ? "text-slate-600" : "text-slate-400"}`}> <p className={`px-4 py-3 text-xs text-center ${isDark ? "text-slate-600" : "text-slate-400"}`}>
Sin resultados Sin resultados
@@ -151,24 +167,40 @@ export const Dropdown: React.FC<DropdownProps> = ({
key={opt.value} key={opt.value}
type="button" type="button"
onClick={() => toggle(opt.value)} onClick={() => toggle(opt.value)}
className={`w-full flex items-center gap-3 px-4 py-3 text-sm transition-colors text-left ${ className={`w-full flex items-center gap-3 px-4 py-3 text-sm transition-colors text-left outline-none ${
isActive isActive
? isDark ? "bg-violet-600/15 text-violet-300" : "bg-violet-50 text-violet-700" ? isDark
: isDark ? "text-slate-300 hover:bg-slate-700/50" : "text-slate-700 hover:bg-slate-50" ? "bg-violet-600/15 text-violet-300"
: "bg-violet-50 text-violet-700"
: isDark
? "text-slate-300 hover:bg-slate-700/50"
: "text-slate-700 hover:bg-slate-50"
}`}
>
<span
className={`w-4 h-4 rounded flex items-center justify-center border shrink-0 transition-colors ${
isActive
? "bg-violet-600 border-violet-600"
: isDark
? "border-slate-600"
: "border-slate-300"
}`} }`}
> >
<span className={`w-4 h-4 rounded flex items-center justify-center border shrink-0 transition-colors ${
isActive ? "bg-violet-600 border-violet-600" : isDark ? "border-slate-600" : "border-slate-300"
}`}>
{isActive && <FiCheck className="text-white text-[10px]" />} {isActive && <FiCheck className="text-white text-[10px]" />}
</span> </span>
<span className="flex-1 font-medium">{opt.label}</span> <span className="flex-1 font-medium">{opt.label}</span>
{opt.count !== undefined && ( {opt.count !== undefined && (
<span className={`text-[11px] px-1.5 py-0.5 rounded font-mono shrink-0 ${ <span
className={`text-[11px] px-1.5 py-0.5 rounded font-mono shrink-0 ${
isActive isActive
? isDark ? "bg-violet-500/30 text-violet-300" : "bg-violet-100 text-violet-600" ? isDark
: isDark ? "bg-slate-700 text-slate-500" : "bg-slate-100 text-slate-400" ? "bg-violet-500/30 text-violet-300"
}`}> : "bg-violet-100 text-violet-600"
: isDark
? "bg-slate-700 text-slate-500"
: "bg-slate-100 text-slate-400"
}`}
>
{opt.count} {opt.count}
</span> </span>
)} )}
@@ -180,19 +212,23 @@ export const Dropdown: React.FC<DropdownProps> = ({
{value.length > 0 && ( {value.length > 0 && (
<> <>
<div className={`h-px ${divider}`} /> <div className={`h-px w-full ${divider}`} />
<div className="px-4 py-2.5 flex justify-end"> <div className="px-4 py-2.5 flex justify-end w-full">
<button <button
type="button"
onClick={() => onChange([])} onClick={() => onChange([])}
className={`text-xs font-medium transition-colors ${isDark ? "text-slate-500 hover:text-slate-300" : "text-slate-400 hover:text-slate-600"}`} className={`text-xs font-medium transition-colors outline-none ${
isDark ? "text-slate-500 hover:text-slate-300" : "text-slate-400 hover:text-slate-600"
}`}
> >
Limpiar selección Limpiar selección
</button> </button>
</div> </div>
</> </>
)} )}
</div> </HeroUIPopover.Dialog>
)} </HeroUIPopover.Content>
</HeroUIPopover.Root>
</div> </div>
); );
}; };
+141 -27
View File
@@ -1,6 +1,8 @@
"use client"; "use client";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { FiUpload, FiX } from "react-icons/fi"; import { FiUpload, FiX } from "react-icons/fi";
import { Button, CloseButton, Label } from "@heroui/react";
import { useTheme } from "@/context/ThemeContext"; import { useTheme } from "@/context/ThemeContext";
export interface ImageUploadProps { export interface ImageUploadProps {
@@ -11,8 +13,15 @@ export interface ImageUploadProps {
hint?: string; hint?: string;
} }
export function ImageUpload({ label, value, onChange, aspectRatio = "square", hint }: ImageUploadProps) { export function ImageUpload({
label,
value,
onChange,
aspectRatio = "square",
hint,
}: ImageUploadProps) {
const { isDark } = useTheme(); const { isDark } = useTheme();
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const [dragging, setDragging] = useState(false); const [dragging, setDragging] = useState(false);
@@ -20,34 +29,118 @@ export function ImageUpload({ label, value, onChange, aspectRatio = "square", hi
const handleFile = (file: File) => { const handleFile = (file: File) => {
if (!file.type.startsWith("image/")) return; if (!file.type.startsWith("image/")) return;
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => onChange(e.target?.result as string);
reader.onload = (event) => {
const result = event.target?.result;
if (typeof result === "string") {
onChange(result);
}
};
reader.readAsDataURL(file); reader.readAsDataURL(file);
}; };
const handleDrop = (e: React.DragEvent) => { const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
e.preventDefault(); event.preventDefault();
setDragging(false); setDragging(false);
const file = e.dataTransfer.files[0];
if (file) handleFile(file); const file = event.dataTransfer.files[0];
if (file) {
handleFile(file);
}
}; };
const border = isDark ? "border-slate-700" : "border-slate-200"; const borderColor = isDark
const bg = isDark ? "bg-slate-800/50" : "bg-slate-50"; ? "border-slate-700"
const active = dragging ? (isDark ? "border-violet-500 bg-violet-500/10" : "border-violet-400 bg-violet-50") : ""; : "border-slate-200";
const muted = isDark ? "text-slate-400" : "text-slate-500";
const backgroundColor = isDark
? "bg-slate-800/50"
: "bg-slate-50";
const dragActiveStyles = dragging
? isDark
? "border-violet-500 bg-violet-500/10"
: "border-violet-400 bg-violet-50"
: "";
const mutedColor = isDark
? "text-slate-400"
: "text-slate-500";
const labelColor = isDark
? "text-slate-300"
: "text-slate-700";
const pickerSize = isBanner
? "h-32 w-full"
: "h-28 w-28";
const openFilePicker = () => {
inputRef.current?.click();
};
const handleInputChange = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const file = event.target.files?.[0];
if (file) {
handleFile(file);
}
};
const handleRemove = () => {
onChange(undefined);
// Permite volver a seleccionar la misma imagen
// después de eliminarla.
if (inputRef.current) {
inputRef.current.value = "";
}
};
return ( return (
<div> <div>
<label className={`block text-sm font-medium mb-1.5 ${isDark ? "text-slate-300" : "text-slate-700"}`}> <Label
className={`mb-1.5 block text-sm font-medium ${labelColor}`}
>
{label} {label}
</label> </Label>
<div <div
className={`relative rounded-xl border-2 border-dashed transition-all duration-200 overflow-hidden cursor-pointer ${border} ${bg} ${active} ${isBanner ? "h-32" : "h-28 w-28"}`} role="button"
onClick={() => inputRef.current?.click()} tabIndex={0}
onDragOver={(e) => { e.preventDefault(); setDragging(true); }} aria-label={`Seleccionar imagen para ${label}`}
onDragLeave={() => setDragging(false)} className={[
"relative cursor-pointer overflow-hidden rounded-xl",
"border-2 border-dashed",
"transition-all duration-200",
"focus-visible:outline-none focus-visible:ring-2",
"focus-visible:ring-violet-500",
borderColor,
backgroundColor,
dragActiveStyles,
pickerSize,
].join(" ")}
onClick={openFilePicker}
onKeyDown={(event) => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
openFilePicker();
}
}}
onDragOver={(event) => {
event.preventDefault();
setDragging(true);
}}
onDragLeave={() => {
setDragging(false);
}}
onDrop={handleDrop} onDrop={handleDrop}
> >
{value ? ( {value ? (
@@ -55,34 +148,55 @@ export function ImageUpload({ label, value, onChange, aspectRatio = "square", hi
<img <img
src={value} src={value}
alt={label} alt={label}
className="w-full h-full object-cover" className="h-full w-full object-cover"
/> />
<button
type="button" <div
onClick={(e) => { e.stopPropagation(); onChange(undefined); }} className="absolute right-2 top-2"
className="absolute top-2 right-2 p-1 rounded-full bg-black/60 text-white hover:bg-black/80 transition" onClick={(event) => event.stopPropagation()}
>
<CloseButton
aria-label="Eliminar imagen"
onPress={handleRemove}
className="rounded-full bg-black/60 text-white hover:bg-black/80"
> >
<FiX className="text-xs" /> <FiX className="text-xs" />
</button> </CloseButton>
</div>
</> </>
) : ( ) : (
<div className="flex flex-col items-center justify-center h-full gap-1.5 px-4 text-center"> <div className="flex h-full flex-col items-center justify-center gap-1.5 px-4 text-center">
<FiUpload className={`text-xl ${muted}`} /> <FiUpload className={`text-xl ${mutedColor}`} />
<p className={`text-xs ${muted}`}>
<p className={`text-xs ${mutedColor}`}>
{dragging ? "Suelta aquí" : "Click o arrastra"} {dragging ? "Suelta aquí" : "Click o arrastra"}
</p> </p>
<Button
type="button"
variant="ghost"
size="sm"
onPress={openFilePicker}
className="mt-1"
>
Seleccionar
</Button>
</div> </div>
)} )}
</div> </div>
{hint && <p className={`text-[11px] mt-1 ${muted}`}>{hint}</p>} {hint && (
<p className={`mt-1 text-[11px] ${mutedColor}`}>
{hint}
</p>
)}
<input <input
ref={inputRef} ref={inputRef}
type="file" type="file"
accept="image/*" accept="image/*"
className="hidden" className="hidden"
onChange={(e) => { const f = e.target.files?.[0]; if (f) handleFile(f); }} onChange={handleInputChange}
/> />
</div> </div>
); );
+79 -85
View File
@@ -1,8 +1,9 @@
"use client"; import React, { useState } from "react";
import React, { useState } from 'react';
import { FiEye, FiEyeOff, FiX, FiAlertCircle } from 'react-icons/fi';
export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> { import { Input as HeroInput, TextField, Label, Description, FieldError, InputGroup } from "@heroui/react";
import { FiEye, FiEyeOff, FiX, FiAlertCircle } from "react-icons/fi";
export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size"> {
label?: string; label?: string;
helperText?: string; helperText?: string;
errorMessage?: string; errorMessage?: string;
@@ -10,28 +11,28 @@ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElem
rightIcon?: React.ReactNode; rightIcon?: React.ReactNode;
isClearable?: boolean; isClearable?: boolean;
onClear?: () => void; onClear?: () => void;
sizeVariant?: 'sm' | 'md' | 'lg'; sizeVariant?: "sm" | "md" | "lg";
fullWidth?: boolean; fullWidth?: boolean;
} }
const sizeStyles = { const sizeStyles = {
sm: { sm: {
input: 'py-2 text-xs rounded-lg', input: "py-2 text-xs rounded-lg",
icon: 'text-sm', icon: "text-sm",
leftPad: 'pl-8', leftPad: "pl-8",
rightPad: 'pr-8', rightPad: "pr-8",
}, },
md: { md: {
input: 'py-2.5 text-sm rounded-xl', input: "py-2.5 text-sm rounded-xl",
icon: 'text-base', icon: "text-base",
leftPad: 'pl-10', leftPad: "pl-10",
rightPad: 'pr-10', rightPad: "pr-10",
}, },
lg: { lg: {
input: 'py-3.5 text-base rounded-2xl', input: "py-3.5 text-base rounded-2xl",
icon: 'text-lg', icon: "text-lg",
leftPad: 'pl-12', leftPad: "pl-12",
rightPad: 'pr-12', rightPad: "pr-12",
}, },
}; };
@@ -45,65 +46,72 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
rightIcon, rightIcon,
isClearable = false, isClearable = false,
onClear, onClear,
sizeVariant = 'md', sizeVariant = "md",
fullWidth = true, fullWidth = true,
type = 'text', type = "text",
id, id,
name, name,
disabled, disabled,
required, required,
className = '', className = "",
value, value,
...props ...props
}, },
ref ref,
) => { ) => {
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const inputId = id || name || (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined); const inputId = id || name || (label ? label.toLowerCase().replace(/\s+/g, "-") : undefined);
const isPasswordType = type === "password";
const isPasswordType = type === 'password'; const computedType = isPasswordType ? (showPassword ? "text" : "password") : type;
const computedType = isPasswordType ? (showPassword ? 'text' : 'password') : type;
const hasError = Boolean(errorMessage); const hasError = Boolean(errorMessage);
const hasValue = value !== undefined && value !== '' && value !== null; const hasValue = value !== undefined && value !== "" && value !== null;
const currentSize = sizeStyles[sizeVariant]; const currentSize = sizeStyles[sizeVariant];
return ( return (
<div className={`${fullWidth ? 'w-full' : 'inline-block'} space-y-1.5`}> <TextField
id={`${inputId}-field`}
isInvalid={hasError}
isDisabled={disabled}
isRequired={required}
className={`${fullWidth ? "w-full" : "inline-block"} space-y-1.5`}>
{/* Label */} {/* Label */}
{label && ( {label && (
<div className="flex items-center justify-between"> <Label
<label
htmlFor={inputId} htmlFor={inputId}
className={`block text-xs font-semibold select-none transition-colors ${ className={`block text-xs font-semibold select-none transition-colors ${
hasError hasError ? "text-rose-600 dark:text-rose-400" : "text-slate-700 dark:text-slate-300"
? 'text-rose-600 dark:text-rose-400' }`}>
: 'text-slate-700 dark:text-slate-300'
}`}
>
{label} {label}
{required && <span className="ml-1 text-rose-500 font-bold">*</span>} {required}
</label> </Label>
</div>
)} )}
{/* Input Wrapper */} {/* Input Wrapper */}
<div className="relative flex items-center"> <div className="relative flex items-center w-full">
<InputGroup
className={`
flex items-center w-full overflow-hidden transition-all duration-200 outline-none
bg-white border border-slate-300 hover:border-slate-400
focus-within:border-cyan-500 focus-within:ring-4 focus-within:ring-cyan-500/15
dark:bg-slate-900/90 dark:border-slate-800 dark:hover:border-slate-700
dark:focus-within:border-cyan-500 dark:focus-within:ring-cyan-500/25
${disabled ? "opacity-60 bg-slate-100 dark:bg-slate-950 cursor-not-allowed" : ""}
${hasError ? "border-rose-500! ring-rose-500/20! focus-within:border-rose-500! focus-within:ring-rose-500/20!" : ""}
${currentSize.input} `}>
{/* Left Icon */} {/* Left Icon */}
{leftIcon && ( {leftIcon && (
<div <InputGroup.Prefix
className={`absolute left-3.5 pointer-events-none flex items-center justify-center transition-colors ${ className={`flex items-center justify-center pl-3.5 pr-2 shrink-0 pointer-events-none transition-colors ${
hasError hasError ? "text-rose-500" : "text-slate-400 dark:text-slate-500"
? 'text-rose-500' } ${currentSize.icon}`}>
: 'text-slate-400 dark:text-slate-500'
} ${currentSize.icon}`}
>
{leftIcon} {leftIcon}
</div> </InputGroup.Prefix>
)} )}
{/* Core Input Element */} {/* Core Input Element */}
<input <HeroInput
ref={ref} ref={ref}
id={inputId} id={inputId}
name={name} name={name}
@@ -112,78 +120,64 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
disabled={disabled} disabled={disabled}
required={required} required={required}
className={` className={`
w-full px-3.5 font-normal transition-all duration-200 outline-none flex-1 w-full bg-transparent border-none outline-none focus:ring-0 p-0 m-0 shadow-none text-slate-900 placeholder:text-slate-400
bg-white text-slate-900 placeholder:text-slate-400 dark:text-slate-100 dark:placeholder:text-slate-500
border border-slate-300 ${hasError ? "text-rose-900 dark:text-rose-200" : ""}
hover:border-slate-400 ${!leftIcon ? "pl-3.5" : ""}
focus:border-cyan-500 focus:ring-4 focus:ring-cyan-500/15 ${!rightIcon && !isClearable && !isPasswordType ? "pr-3.5" : ""}
dark:bg-slate-900/90 dark:text-slate-100 dark:placeholder:text-slate-500
dark:border-slate-800 dark:hover:border-slate-700
dark:focus:border-cyan-500 dark:focus:ring-cyan-500/25
disabled:opacity-60 disabled:bg-slate-100 disabled:dark:bg-slate-950 disabled:cursor-not-allowed
${leftIcon ? currentSize.leftPad : ''}
${isPasswordType || isClearable || rightIcon ? currentSize.rightPad : ''}
${
hasError
? '!border-rose-500 !ring-rose-500/20 text-rose-900 dark:text-rose-200 focus:!border-rose-500 focus:!ring-rose-500/20'
: ''
}
${currentSize.input}
${className} ${className}
`} `}
{...props} {...props}
/> />
{/* Right Action Icons (Clear, Password Toggle, Custom RightIcon) */} {/* Right Action Icons */}
<div className="absolute right-3 flex items-center gap-1.5 text-slate-400 dark:text-slate-500"> <InputGroup.Suffix className="flex items-center gap-1.5 pr-3 pl-1 shrink-0 text-slate-400 dark:text-slate-500">
{/* Clear */}
{isClearable && hasValue && !disabled && ( {isClearable && hasValue && !disabled && (
<button <button
type="button" type="button"
tabIndex={-1} tabIndex={-1}
onClick={onClear} onClick={onClear}
aria-label="Limpiar campo" className="p-1 rounded-md hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-slate-200 transition-colors">
className="p-1 rounded-md hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-slate-200 transition-colors"
>
<FiX className="text-sm" /> <FiX className="text-sm" />
</button> </button>
)} )}
{/* Password Toggle */}
{isPasswordType && !disabled && ( {isPasswordType && !disabled && (
<button <button
type="button" type="button"
tabIndex={-1} tabIndex={-1}
onClick={() => setShowPassword((prev) => !prev)} onClick={() => setShowPassword((prev) => !prev)}
aria-label={showPassword ? 'Ocultar contraseña' : 'Ver contraseña'} className="p-1 rounded-md hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-slate-200 transition-colors">
className="p-1 rounded-md hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-slate-200 transition-colors" {showPassword ? <FiEyeOff className={currentSize.icon} /> : <FiEye className={currentSize.icon} />}
>
{showPassword ? (
<FiEyeOff className={currentSize.icon} />
) : (
<FiEye className={currentSize.icon} />
)}
</button> </button>
)} )}
{/* Custom Right Icon */}
{!isPasswordType && rightIcon && ( {!isPasswordType && rightIcon && (
<div className={`pointer-events-none flex items-center justify-center ${currentSize.icon}`}> <div className={`pointer-events-none flex items-center justify-center ${currentSize.icon}`}>
{rightIcon} {rightIcon}
</div> </div>
)} )}
</div> </InputGroup.Suffix>
</InputGroup>
</div> </div>
{/* Error or Helper Message */} {/* Error or Helper Message */}
{hasError ? ( {hasError ? (
<p className="flex items-center gap-1.5 text-xs text-rose-600 dark:text-rose-400 animate-fadeIn"> <FieldError className="flex items-center gap-1.5 text-xs text-rose-600 dark:text-rose-400 animate-fadeIn">
<FiAlertCircle className="shrink-0 text-xs" /> <FiAlertCircle className="shrink-0 text-xs" />
<span>{errorMessage}</span> <span>{errorMessage}</span>
</p> </FieldError>
) : helperText ? ( ) : helperText ? (
<p className="text-xs text-slate-500 dark:text-slate-400">{helperText}</p> <Description className="text-xs text-slate-500 dark:text-slate-400">{helperText}</Description>
) : null} ) : null}
</div> </TextField>
); );
} },
); );
Input.displayName = 'Input'; Input.displayName = "Input";
+44 -69
View File
@@ -1,13 +1,15 @@
"use client"; "use client";
import React, { useEffect } from 'react';
import { FiX } from 'react-icons/fi'; import React from "react";
import { Modal as HeroModal } from "@heroui/react";
import { FiX } from "react-icons/fi";
export interface ModalProps { export interface ModalProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
title?: React.ReactNode; title?: React.ReactNode;
description?: React.ReactNode; description?: React.ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full'; size?: "sm" | "md" | "lg" | "xl" | "2xl" | "full";
children: React.ReactNode; children: React.ReactNode;
footer?: React.ReactNode; footer?: React.ReactNode;
closeOnOverlayClick?: boolean; closeOnOverlayClick?: boolean;
@@ -16,12 +18,12 @@ export interface ModalProps {
} }
const sizeStyles = { const sizeStyles = {
sm: 'max-w-md', sm: "max-w-md",
md: 'max-w-lg', md: "max-w-lg",
lg: 'max-w-2xl', lg: "max-w-2xl",
xl: 'max-w-4xl', xl: "max-w-4xl",
'2xl': 'max-w-6xl', "2xl": "max-w-6xl",
full: 'max-w-[95vw] h-[90vh]', full: "max-w-[95vw] h-[90vh]",
}; };
export const Modal: React.FC<ModalProps> = ({ export const Modal: React.FC<ModalProps> = ({
@@ -29,101 +31,74 @@ export const Modal: React.FC<ModalProps> = ({
onClose, onClose,
title, title,
description, description,
size = 'md', size = "md",
children, children,
footer, footer,
closeOnOverlayClick = true, closeOnOverlayClick = true,
showCloseButton = true, showCloseButton = true,
className = '', className = "",
}) => { }) => {
// ESC key handler and body scroll lock return (
useEffect(() => { <HeroModal>
if (!isOpen) return; <HeroModal.Backdrop
isOpen={isOpen}
const handleKeyDown = (e: KeyboardEvent) => { isDismissable={closeOnOverlayClick}
if (e.key === 'Escape') { onOpenChange={(open) => {
if (!open) {
onClose(); onClose();
} }
};
const originalOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
window.addEventListener('keydown', handleKeyDown);
return () => {
document.body.style.overflow = originalOverflow;
window.removeEventListener('keydown', handleKeyDown);
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div
role="dialog"
aria-modal="true"
className="fixed inset-0 z-50 flex items-center justify-center p-4 sm:p-6 overflow-y-auto"
>
{/* Backdrop */}
<div
onClick={() => {
if (closeOnOverlayClick) onClose();
}} }}
className="fixed inset-0 bg-slate-950/70 backdrop-blur-md transition-opacity animate-fadeIn" variant="blur"
/> className="fixed inset-0 bg-slate-950/70 backdrop-blur-md transition-opacity animate-fadeIn">
<HeroModal.Container placement="center" scroll="inside" size="md" className="p-4 sm:p-6">
{/* Modal Dialog Card */} <HeroModal.Dialog
<div aria-modal="true"
className={` className={`
relative w-full rounded-3xl overflow-hidden shadow-2xl z-10 relative w-full rounded-3xl overflow-hidden shadow-2xl z-10 p-0
bg-white text-slate-900 border border-slate-200 bg-white text-slate-900 border border-slate-200
dark:bg-slate-900/95 dark:text-slate-100 dark:border-slate-800 dark:bg-slate-900/95 dark:text-slate-100 dark:border-slate-800
transition-all transform animate-scaleUp transition-all transform animate-scaleUp
${sizeStyles[size]} ${sizeStyles[size]}
${className} ${className}
`} `}>
>
{/* Header */} {/* Header */}
{(title || description || showCloseButton) && ( {(title || description || showCloseButton) && (
<div className="flex items-start justify-between p-6 pb-4 border-b border-slate-100 dark:border-slate-800/80"> <HeroModal.Header className="flex items-start justify-between p-6 pb-4 border-b border-slate-100 dark:border-slate-800/80">
<div className="space-y-1 pr-6"> <div className="space-y-1 pr-6">
{title && ( {title && (
<h2 className="text-xl font-bold text-slate-900 dark:text-white tracking-tight"> <HeroModal.Heading className="text-xl font-bold text-slate-900 dark:text-white tracking-tight">
{title} {title}
</h2> </HeroModal.Heading>
)} )}
{description && ( {description && (
<p className="text-xs sm:text-sm text-slate-500 dark:text-slate-400"> <p className="text-xs sm:text-sm text-slate-500 dark:text-slate-400">{description}</p>
{description}
</p>
)} )}
</div> </div>
{showCloseButton && ( {showCloseButton && (
<button <HeroModal.CloseTrigger
type="button"
onClick={onClose}
aria-label="Cerrar modal"
className="p-2 rounded-xl text-slate-400 hover:text-slate-700 hover:bg-slate-100 dark:hover:text-slate-200 dark:hover:bg-slate-800 transition-colors" className="p-2 rounded-xl text-slate-400 hover:text-slate-700 hover:bg-slate-100 dark:hover:text-slate-200 dark:hover:bg-slate-800 transition-colors"
> onPress={onClose}
aria-label="Cerrar modal">
<FiX className="text-lg" /> <FiX className="text-lg" />
</button> </HeroModal.CloseTrigger>
)} )}
</div> </HeroModal.Header>
)} )}
{/* Body Content */} {/* Body Content */}
<div className="p-6 overflow-y-auto max-h-[calc(85vh-160px)]"> <HeroModal.Body className="p-6 overflow-y-auto max-h-[calc(85vh-160px)]">{children}</HeroModal.Body>
{children}
</div>
{/* Footer */} {/* Footer */}
{footer && ( {footer && (
<div className="p-6 pt-4 border-t border-slate-100 dark:border-slate-800/80 bg-slate-50/50 dark:bg-slate-950/40 flex items-center justify-end gap-3"> <HeroModal.Footer className="p-6 pt-4 border-t border-slate-100 dark:border-slate-800/80 bg-slate-50/50 dark:bg-slate-950/40 flex items-center justify-end gap-3">
{footer} {footer}
</div> </HeroModal.Footer>
)} )}
</div> </HeroModal.Dialog>
</div> </HeroModal.Container>
</HeroModal.Backdrop>
</HeroModal>
); );
}; };
+136 -101
View File
@@ -1,6 +1,9 @@
"use client"; "use client";
import React from 'react';
import { FiChevronDown, FiAlertCircle } from 'react-icons/fi'; import React from "react";
import { Select as HeroSelect, Label, Description, FieldError, ListBox, ListBoxItem } from "@heroui/react";
import type { Key } from "@heroui/react";
import { FiChevronDown, FiAlertCircle } from "react-icons/fi";
export interface SelectOption { export interface SelectOption {
value: string | number; value: string | number;
@@ -8,158 +11,190 @@ export interface SelectOption {
disabled?: boolean; disabled?: boolean;
} }
export interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'size'> { export interface SelectProps {
label?: string; label?: string;
helperText?: string; helperText?: string;
errorMessage?: string; errorMessage?: string;
leftIcon?: React.ReactNode; leftIcon?: React.ReactNode;
options?: SelectOption[]; options?: SelectOption[];
sizeVariant?: 'sm' | 'md' | 'lg'; sizeVariant?: "sm" | "md" | "lg";
fullWidth?: boolean; fullWidth?: boolean;
id?: string;
name?: string;
disabled?: boolean;
required?: boolean;
value?: string | number;
defaultValue?: string | number;
onChange?: (value: string | null) => void;
className?: string;
children?: React.ReactNode;
} }
const sizeStyles = { const sizeStyles = {
sm: { sm: {
select: 'py-2 text-xs rounded-lg', select: "py-2 text-xs rounded-lg",
icon: 'text-sm', icon: "text-sm",
leftPad: 'pl-8', leftPad: "pl-8",
rightPad: 'pr-8', rightPad: "pr-8",
}, },
md: { md: {
select: 'py-2.5 text-sm rounded-xl', select: "py-2.5 text-sm rounded-xl",
icon: 'text-base', icon: "text-base",
leftPad: 'pl-10', leftPad: "pl-10",
rightPad: 'pr-10', rightPad: "pr-10",
}, },
lg: { lg: {
select: 'py-3.5 text-base rounded-2xl', select: "py-3.5 text-base rounded-2xl",
icon: 'text-lg', icon: "text-lg",
leftPad: 'pl-12', leftPad: "pl-12",
rightPad: 'pr-12', rightPad: "pr-12",
}, },
}; };
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>( export const Select = React.forwardRef<HTMLDivElement, SelectProps>(
( (
{ {
label, label,
helperText, helperText,
errorMessage, errorMessage,
leftIcon, leftIcon,
options, options = [],
sizeVariant = 'md', sizeVariant = "md",
fullWidth = true, fullWidth = true,
id, id,
name, name,
disabled, disabled,
required, required,
className = '', className = "",
value,
defaultValue,
onChange,
children, children,
...props
}, },
ref ref,
) => { ) => {
const selectId = id || name || (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined); const selectId = id || name || (label ? label.toLowerCase().replace(/\s+/g, "-") : undefined);
const hasError = Boolean(errorMessage); const hasError = Boolean(errorMessage);
const currentSize = sizeStyles[sizeVariant]; const currentSize = sizeStyles[sizeVariant];
const selectedKey = value !== undefined && value !== null && value !== "" ? String(value) : undefined;
const defaultSelectedKey =
defaultValue !== undefined && defaultValue !== null && defaultValue !== "" ? String(defaultValue) : undefined;
const handleSelectionChange = (key: Key | null) => {
if (!onChange) return;
onChange(key !== null ? String(key) : null);
};
return ( return (
<div className={`${fullWidth ? 'w-full' : 'inline-block'} space-y-1.5`}> <div className={`${fullWidth ? "w-full" : "inline-block"} space-y-1.5`}>
{/* Label */} <HeroSelect
{label && (
<div className="flex items-center justify-between">
<label
htmlFor={selectId}
className={`block text-xs font-semibold select-none transition-colors ${
hasError
? 'text-rose-600 dark:text-rose-400'
: 'text-slate-700 dark:text-slate-300'
}`}
>
{label}
{required && <span className="ml-1 text-rose-500 font-bold">*</span>}
</label>
</div>
)}
{/* Select Wrapper */}
<div className="relative flex items-center">
{/* Left Icon */}
{leftIcon && (
<div
className={`absolute left-3.5 pointer-events-none flex items-center justify-center transition-colors ${
hasError
? 'text-rose-500'
: 'text-slate-400 dark:text-slate-500'
} ${currentSize.icon}`}
>
{leftIcon}
</div>
)}
{/* Core Select Element */}
<select
ref={ref} ref={ref}
id={selectId} id={selectId}
name={name} name={name}
disabled={disabled} selectedKey={selectedKey}
required={required} defaultSelectedKey={defaultSelectedKey}
onSelectionChange={handleSelectionChange}
isDisabled={disabled}
isRequired={required}
isInvalid={hasError}
className="w-full">
{label && (
<Label
className={`block text-xs font-semibold select-none transition-colors ${
hasError ? "text-rose-600 dark:text-rose-400" : "text-slate-700 dark:text-slate-300"
}`}>
{label}
{required && <span className="ml-1 text-rose-500 font-bold">*</span>}
</Label>
)}
<HeroSelect.Trigger
className={` className={`
w-full px-3.5 font-normal transition-all duration-200 outline-none appearance-none cursor-pointer relative flex w-full items-center
font-normal transition-all duration-200
outline-none appearance-none cursor-pointer
bg-white text-slate-900 bg-white text-slate-900
border border-slate-300 border border-slate-300
hover:border-slate-400 hover:border-slate-400
focus:border-cyan-500 focus:ring-4 focus:ring-cyan-500/15 focus:border-cyan-500
dark:bg-slate-900/90 dark:text-slate-100 focus:ring-4
dark:border-slate-800 dark:hover:border-slate-700 focus:ring-cyan-500/15
dark:focus:border-cyan-500 dark:focus:ring-cyan-500/25 dark:bg-slate-900/90
disabled:opacity-60 disabled:bg-slate-100 disabled:dark:bg-slate-950 disabled:cursor-not-allowed dark:text-slate-100
${leftIcon ? currentSize.leftPad : ''} dark:border-slate-800
${currentSize.rightPad} dark:hover:border-slate-700
${ dark:focus:border-cyan-500
hasError dark:focus:ring-cyan-500/25
? '!border-rose-500 !ring-rose-500/20 text-rose-900 dark:text-rose-200 focus:!border-rose-500 focus:!ring-rose-500/20' disabled:opacity-60
: '' disabled:bg-slate-100
} disabled:dark:bg-slate-950
disabled:cursor-not-allowed
${currentSize.select} ${currentSize.select}
${leftIcon ? currentSize.leftPad : ""}
${currentSize.rightPad}
${hasError ? "border-rose-500! ring-rose-500/20! text-rose-900 dark:text-rose-200" : ""}
${className} ${className}
`} `}>
{...props} {leftIcon && (
> <span
{options className={`
? options.map((opt) => ( absolute left-3.5
<option pointer-events-none
key={opt.value} flex items-center justify-center
value={opt.value} transition-colors
disabled={opt.disabled} ${hasError ? "text-rose-500" : "text-slate-400 dark:text-slate-500"}
className="bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100" ${currentSize.icon}
> `}>
{opt.label} {leftIcon}
</option> </span>
)) )}
: children}
</select>
{/* Right Chevron Icon */} <HeroSelect.Value className="w-full text-left" />
<div
className={`absolute right-3.5 pointer-events-none flex items-center justify-center text-slate-400 dark:text-slate-500 ${currentSize.icon}`} <HeroSelect.Indicator
> className={`
absolute right-3.5
pointer-events-none
flex items-center justify-center
text-slate-400 dark:text-slate-500
${currentSize.icon}
`}>
<FiChevronDown /> <FiChevronDown />
</div> </HeroSelect.Indicator>
</div> </HeroSelect.Trigger>
<HeroSelect.Popover className="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-xl overflow-hidden z-50 min-w-(--trigger-width)">
<ListBox className="p-1 outline-none">
{options.length > 0
? options.map((option) => (
<ListBoxItem
key={String(option.value)}
id={String(option.value)}
textValue={option.label}
isDisabled={option.disabled}
className="px-3 py-2 text-sm rounded-lg cursor-pointer hover:bg-slate-100 dark:hover:bg-slate-800 data-[selected=true]:bg-cyan-500/10 data-[selected=true]:text-cyan-600 dark:data-[selected=true]:text-cyan-400 outline-none transition-colors">
{option.label}
</ListBoxItem>
))
: (children as any)}
</ListBox>
</HeroSelect.Popover>
{/* Error or Helper Message */}
{hasError ? ( {hasError ? (
<p className="flex items-center gap-1.5 text-xs text-rose-600 dark:text-rose-400 animate-fadeIn"> <FieldError className="flex items-center gap-1.5 text-xs text-rose-600 dark:text-rose-400">
<FiAlertCircle className="shrink-0 text-xs" /> <FiAlertCircle className="shrink-0 text-xs" />
<span>{errorMessage}</span> <span>{errorMessage}</span>
</p> </FieldError>
) : helperText ? ( ) : helperText ? (
<p className="text-xs text-slate-500 dark:text-slate-400">{helperText}</p> <Description className="text-xs text-slate-500 dark:text-slate-400">{helperText}</Description>
) : null} ) : null}
</HeroSelect>
</div> </div>
); );
} },
); );
Select.displayName = 'Select'; Select.displayName = "Select";
+21 -25
View File
@@ -1,45 +1,41 @@
"use client"; "use client";
import React from 'react';
import React from "react";
import { Spinner as HeroSpinner } from "@heroui/react";
export interface SpinnerProps { export interface SpinnerProps {
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; size?: "xs" | "sm" | "md" | "lg" | "xl";
color?: 'cyan' | 'blue' | 'white' | 'slate' | 'current'; color?: "cyan" | "blue" | "white" | "slate" | "current";
className?: string; className?: string;
label?: string; label?: string;
} }
const sizeClasses = { const sizeClasses = {
xs: 'w-3.5 h-3.5 border-2', xs: "w-3.5 h-3.5",
sm: 'w-4 h-4 border-2', sm: "w-4 h-4",
md: 'w-6 h-6 border-2', md: "w-6 h-6",
lg: 'w-8 h-8 border-3', lg: "w-8 h-8",
xl: 'w-12 h-12 border-4', xl: "w-12 h-12",
}; };
const colorClasses = { const colorClasses = {
cyan: 'border-cyan-500/20 border-t-cyan-500 dark:border-cyan-400/20 dark:border-t-cyan-400', cyan: "text-cyan-500 dark:text-cyan-400",
blue: 'border-blue-500/20 border-t-blue-500 dark:border-blue-400/20 dark:border-t-blue-400', blue: "text-blue-500 dark:text-blue-400",
white: 'border-white/30 border-t-white', white: "text-white",
slate: 'border-slate-300 border-t-slate-700 dark:border-slate-700 dark:border-t-slate-200', slate: "text-slate-700 dark:text-slate-200",
current: 'border-current/20 border-t-current', current: "text-current",
}; };
export const Spinner: React.FC<SpinnerProps> = ({ export const Spinner: React.FC<SpinnerProps> = ({
size = 'md', size = "md",
color = 'cyan', color = "cyan",
className = '', className = "",
label = 'Cargando...', label = "Cargando...",
}) => { }) => {
return ( return (
<div <HeroSpinner
role="status"
aria-label={label} aria-label={label}
className={`inline-flex items-center justify-center ${className}`} className={`inline-block animate-spin ${sizeClasses[size]} ${colorClasses[color]} ${className}`}
>
<div
className={`rounded-full animate-spin ${sizeClasses[size]} ${colorClasses[color]}`}
/> />
<span className="sr-only">{label}</span>
</div>
); );
}; };
+90 -69
View File
@@ -1,5 +1,11 @@
"use client"; "use client";
import React from 'react';
import React from "react";
import {
Switch as HeroSwitch,
Label,
Description,
} from "@heroui/react";
export interface SwitchProps { export interface SwitchProps {
label?: React.ReactNode; label?: React.ReactNode;
@@ -7,7 +13,7 @@ export interface SwitchProps {
checked?: boolean; checked?: boolean;
onChange?: (checked: boolean) => void; onChange?: (checked: boolean) => void;
disabled?: boolean; disabled?: boolean;
size?: 'sm' | 'md' | 'lg'; size?: "sm" | "md" | "lg";
checkedIcon?: React.ReactNode; checkedIcon?: React.ReactNode;
uncheckedIcon?: React.ReactNode; uncheckedIcon?: React.ReactNode;
id?: string; id?: string;
@@ -16,21 +22,21 @@ export interface SwitchProps {
} }
const trackSizes = { const trackSizes = {
sm: 'w-8 h-4.5', sm: "w-8 h-[18px]",
md: 'w-11 h-6', md: "w-11 h-6",
lg: 'w-14 h-7.5', lg: "w-14 h-[30px]",
}; };
const thumbSizes = { const thumbSizes = {
sm: 'w-3.5 h-3.5', sm: "size-3.5",
md: 'w-5 h-5', md: "size-5",
lg: 'w-6.5 h-6.5', lg: "size-[26px]",
}; };
const translateSizes = { const translateSizes = {
sm: 'translate-x-3.5', sm: "translate-x-3.5",
md: 'translate-x-5', md: "translate-x-5",
lg: 'translate-x-6.5', lg: "translate-x-[26px]",
}; };
export const Switch: React.FC<SwitchProps> = ({ export const Switch: React.FC<SwitchProps> = ({
@@ -39,85 +45,100 @@ export const Switch: React.FC<SwitchProps> = ({
checked = false, checked = false,
onChange, onChange,
disabled = false, disabled = false,
size = 'md', size = "md",
checkedIcon, checkedIcon,
uncheckedIcon, uncheckedIcon,
id, id,
name, name,
className = '', className = "",
}) => { }) => {
const switchId = id || name || (typeof label === 'string' ? label.toLowerCase().replace(/\s+/g, '-') : undefined); const switchId =
id ||
const handleClick = () => { name ||
if (!disabled && onChange) { (typeof label === "string"
onChange(!checked); ? label.toLowerCase().replace(/\s+/g, "-")
} : undefined);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
handleClick();
}
};
return ( return (
<div className={`inline-flex items-start gap-3 select-none ${className}`}> <HeroSwitch
{/* Switch Button */}
<button
id={switchId} id={switchId}
type="button" name={name}
role="switch" isSelected={checked}
aria-checked={checked} isDisabled={disabled}
disabled={disabled} size={size}
onClick={handleClick} className={className}
onKeyDown={handleKeyDown} onChange={(e: any) => {
if (!onChange || disabled) return;
const val = typeof e === "boolean" ? e : Boolean(e?.target?.checked);
onChange(val);
}}
>
<HeroSwitch.Content>
<HeroSwitch.Control
className={` className={`
relative shrink-0 inline-flex items-center rounded-full p-0.5 cursor-pointer transition-colors duration-200 ease-in-out
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2
focus-visible:ring-cyan-500 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950
disabled:opacity-50 disabled:cursor-not-allowed
${trackSizes[size]} ${trackSizes[size]}
relative shrink-0 inline-flex items-center rounded-full p-0.5
transition-colors duration-200 ease-in-out cursor-pointer
border-0 m-0 box-border outline-none
${ ${
checked checked
? 'bg-cyan-500 dark:bg-cyan-500' ? "bg-cyan-500"
: 'bg-slate-300 dark:bg-slate-700 hover:bg-slate-400 dark:hover:bg-slate-600' : "bg-slate-300 hover:bg-slate-400 dark:bg-slate-700 dark:hover:bg-slate-600"
}
${disabled ? "cursor-not-allowed opacity-50" : ""}
`}
>
<HeroSwitch.Thumb
className={`
${thumbSizes[size]}
${checked ? translateSizes[size] : "translate-x-0"}
border-0 m-0 pointer-events-none shrink-0 flex items-center justify-center
rounded-full bg-white text-slate-800 shadow-md
transition-transform duration-200 ease-in-out
`}
>
{(checkedIcon || uncheckedIcon) && (
<HeroSwitch.Icon>
{checked ? checkedIcon : uncheckedIcon}
</HeroSwitch.Icon>
)}
</HeroSwitch.Thumb>
</HeroSwitch.Control>
{/* TEXTOS */}
{(label || description) && (
<div className="flex flex-col text-left">
{label && (
<Label
className={`
text-xs sm:text-sm font-medium transition-colors cursor-pointer
${
disabled
? "text-slate-400 dark:text-slate-600 cursor-not-allowed"
: "text-slate-800 hover:text-slate-950 dark:text-slate-200 dark:hover:text-white"
} }
`} `}
> >
{/* Thumb */} {label}
<span </Label>
)}
{description && (
<Description
className={` className={`
pointer-events-none flex items-center justify-center rounded-full bg-white shadow-md transform transition-transform duration-200 ease-in-out text-slate-800 text-xs text-slate-500 dark:text-slate-400 cursor-pointer
${thumbSizes[size]} ${disabled ? "cursor-not-allowed" : ""}
${checked ? translateSizes[size] : 'translate-x-0'}
`} `}
> >
{checked ? checkedIcon : uncheckedIcon}
</span>
</button>
{/* Label and description */}
{(label || description) && (
<div className="flex flex-col text-left cursor-pointer" onClick={handleClick}>
{label && (
<span
className={`text-xs sm:text-sm font-medium transition-colors ${
disabled
? 'text-slate-400 dark:text-slate-600'
: 'text-slate-800 dark:text-slate-200 hover:text-slate-950 dark:hover:text-white'
}`}
>
{label}
</span>
)}
{description && (
<span className="text-xs text-slate-500 dark:text-slate-400">
{description} {description}
</span> </Description>
)} )}
</div> </div>
)} )}
</div> </HeroSwitch.Content>
</HeroSwitch>
); );
}; };