Compare commits

...
13 Commits
16 changed files with 2256 additions and 1200 deletions

No files matched your search

+737 -14
View File
@@ -9,6 +9,8 @@
"version": "0.0.0",
"dependencies": {
"@azure/msal-browser": "^5.21.0",
"@heroui/react": "^3.2.4",
"@heroui/styles": "^3.2.4",
"@react-oauth/google": "^0.13.5",
"@tailwindcss/postcss": "^4.3.3",
"@tanstack/react-query": "^5.101.4",
@@ -35,6 +37,50 @@
"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": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
@@ -99,7 +145,6 @@
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.7",
"@babel/generator": "^7.29.7",
@@ -261,6 +306,15 @@
"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": {
"version": "7.29.7",
"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_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": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -1014,6 +1163,44 @@
"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": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -1193,6 +1380,218 @@
"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": {
"version": "0.13.5",
"resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.13.5.tgz",
@@ -1203,6 +1602,120 @@
"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": {
"version": "0.5.23",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
@@ -1531,7 +2044,6 @@
"integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1540,7 +2052,7 @@
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
@@ -1591,7 +2103,6 @@
"integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.67.0",
"@typescript-eslint/types": "8.67.0",
@@ -1796,7 +2307,6 @@
"integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1843,6 +2353,18 @@
"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": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -1916,7 +2438,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.11.12",
"caniuse-lite": "^1.0.30001809",
@@ -1970,6 +2491,15 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
"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": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -2008,7 +2538,6 @@
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"devOptional": true,
"license": "MIT"
},
"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": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -2053,6 +2589,16 @@
"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": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2161,7 +2707,6 @@
"integrity": "sha512-5KeEOJZBfEVA47boFiBsf+6MmmJpffM7qEBg4pLla2e4nlKgdKlqCW0oSLOGsT8Wl5uCGJptLV1bkaiShj90Gw==",
"dev": true,
"license": "MIT",
"peer": true,
"workspaces": [
"packages/*"
],
@@ -2656,6 +3201,29 @@
"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": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -2699,7 +3267,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/jsesc": {
@@ -3038,6 +3605,18 @@
"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": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -3225,6 +3804,15 @@
"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": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -3307,7 +3895,6 @@
"integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -3353,6 +3940,17 @@
"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": {
"version": "2.1.0",
"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",
"integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
"license": "MIT",
"peer": true,
"engines": {
"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": {
"version": "19.2.8",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
"integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -3404,6 +4040,45 @@
"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": {
"version": "0.27.0",
"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": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz",
@@ -3609,6 +4316,15 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"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": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -3628,7 +4344,6 @@
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -3709,6 +4424,15 @@
"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": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -3761,7 +4485,6 @@
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
+2
View File
@@ -11,6 +11,8 @@
},
"dependencies": {
"@azure/msal-browser": "^5.21.0",
"@heroui/react": "^3.2.4",
"@heroui/styles": "^3.2.4",
"@react-oauth/google": "^0.13.5",
"@tailwindcss/postcss": "^4.3.3",
"@tanstack/react-query": "^5.101.4",
+6 -6
View File
@@ -163,7 +163,7 @@ export default function ComponentsContent() {
<Badge variant="danger" icon={<FiAlertCircle />}>
Con ícono
</Badge>
<Badge variant="primary" removable onRemove={() => {}}>
<Badge variant="primary" removable onRemove={() => { }}>
Removable
</Badge>
</Row>
@@ -279,15 +279,15 @@ export default function ComponentsContent() {
<Row>
<Checkbox
label="Recordar sesión"
checked={checked}
onChange={(e) => setChecked(e.target.checked)}
isSelected={checked}
onChange={(isSelected) => setChecked(isSelected)}
/>
<Checkbox label="Deshabilitado" disabled />
<Checkbox
label="Marcado fijo"
checked
disabled
onChange={() => {}}
onChange={() => { }}
/>
</Row>
<Row>
@@ -302,8 +302,8 @@ export default function ComponentsContent() {
<CodeBlock
code={`<Checkbox
label="Recordar sesión"
checked={checked}
onChange={e => setChecked(e.target.checked)}
isSelected={checked}
onChange={(isSelected) => setChecked(isSelected)}
/>
<Switch
+1
View File
@@ -1,4 +1,5 @@
@import "tailwindcss";
@import "@heroui/styles";
@custom-variant dark (&:is(.dark, .dark *));
+103 -104
View File
@@ -1,18 +1,49 @@
"use client";
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';
export interface BadgeProps {
children: React.ReactNode;
variant?:
| 'default'
| 'primary'
| 'secondary'
| 'success'
| 'warning'
| 'danger'
| 'info'
| 'outline';
// 1. Organizamos los estilos con 'tv' usando variantes estándar
const badgeStyles = tv({
base: "inline-flex items-center select-none border transition-colors",
variants: {
customVariant: {
default: 'bg-slate-100 text-slate-800 border-slate-200 dark:bg-slate-800/80 dark:text-slate-200 dark:border-slate-700',
primary: 'bg-cyan-50 text-cyan-700 border-cyan-200 dark:bg-cyan-950/60 dark:text-cyan-300 dark:border-cyan-500/30',
secondary: 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-950/60 dark:text-purple-300 dark:border-purple-500/30',
success: 'bg-emerald-50 text-emerald-700 border-emerald-200 dark:bg-emerald-950/60 dark:text-emerald-300 dark:border-emerald-500/30',
warning: 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/60 dark:text-amber-300 dark:border-amber-500/30',
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';
dot?: boolean;
dotPulse?: boolean;
@@ -20,103 +51,71 @@ export interface BadgeProps {
removable?: boolean;
onRemove?: () => void;
className?: string;
children?: React.ReactNode;
}
const variantStyles: Record<NonNullable<BadgeProps['variant']>, { badge: string; dot: string }> = {
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',
},
};
export const Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(
(
{
children,
variant = 'default',
size = 'md',
dot = false,
dotPulse = false,
icon,
removable = false,
onRemove,
className = '',
...props
},
ref
) => {
const dotColor = dotColorMap[variant];
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',
};
return (
<HeroUIChip
ref={ref}
// En HeroUI v3 se usa className en singular (soporta string directo desde tv)
className={badgeStyles({
customVariant: variant,
customSize: size,
className
})}
{...props}
>
{/* Optional Dot indicator */}
{dot && (
<span className="relative flex h-1.5 w-1.5 shrink-0">
{dotPulse && (
<span className={`animate-ping absolute inline-flex h-full w-full rounded-full opacity-75 ${dotColor}`} />
)}
<span className={`relative inline-flex rounded-full h-1.5 w-1.5 ${dotColor}`} />
</span>
)}
export const Badge: React.FC<BadgeProps> = ({
children,
variant = 'default',
size = 'md',
dot = false,
dotPulse = false,
icon,
removable = false,
onRemove,
className = '',
}) => {
const currentVariant = variantStyles[variant];
{/* Optional Icon */}
{icon && <span className="shrink-0">{icon}</span>}
return (
<span
className={`
inline-flex items-center select-none border transition-colors
${currentVariant.badge}
${sizeStyles[size]}
${className}
`}
>
{/* Optional Dot indicator */}
{dot && (
<span className="relative flex h-1.5 w-1.5 shrink-0">
{dotPulse && (
<span
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>
)}
{/* Content */}
<span>{children}</span>
{/* Optional Icon */}
{icon && <span className="shrink-0">{icon}</span>}
{/* Optional Remove Button */}
{removable && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onRemove?.();
}}
className="ml-0.5 p-0.5 rounded-full hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
aria-label="Remover etiqueta"
>
<FiX className="text-xs" />
</button>
)}
</HeroUIChip>
);
}
);
{/* Content */}
<span>{children}</span>
{/* Optional Remove Button */}
{removable && (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onRemove?.();
}}
className="ml-0.5 p-0.5 rounded-full hover:bg-black/10 dark:hover:bg-white/10 transition-colors"
aria-label="Remover etiqueta"
>
<FiX className="text-xs" />
</button>
)}
</span>
);
};
Badge.displayName = 'Badge';
+53 -45
View File
@@ -1,8 +1,43 @@
"use client";
import React from 'react';
import { Button as HeroUIButton, ButtonProps as HeroUIButtonProps } from '@heroui/react';
import { tv } from '@heroui/styles';
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';
size?: 'xs' | 'sm' | 'md' | 'lg' | 'icon';
isLoading?: boolean;
@@ -10,35 +45,11 @@ export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
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>(
(
{
@@ -50,30 +61,27 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
rightIcon,
fullWidth = false,
disabled,
isDisabled,
className = '',
children,
...props
},
ref
) => {
const isInteractiveDisabled = disabled || isLoading;
const actuallyDisabled = disabled || isDisabled || isLoading;
return (
<button
<HeroUIButton
ref={ref}
disabled={isInteractiveDisabled}
className={`
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]
disabled:opacity-50 disabled:cursor-not-allowed disabled:pointer-events-none disabled:active:scale-100
${fullWidth ? 'w-full' : ''}
${variantStyles[variant]}
${sizeStyles[size]}
${className}
`}
isDisabled={actuallyDisabled}
className={customButtonStyles({
customVariant: variant,
customSize: size,
fullWidth,
isDisabled: actuallyDisabled,
className
})}
variant={undefined}
{...props}
>
{isLoading ? (
@@ -91,9 +99,9 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
{rightIcon && <span className="shrink-0">{rightIcon}</span>}
</>
)}
</button>
</HeroUIButton>
);
}
);
Button.displayName = 'Button';
Button.displayName = 'Button';
+134 -84
View File
@@ -1,147 +1,197 @@
"use client";
import React from 'react';
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
variant?: 'default' | 'elevated' | 'glass' | 'outline' | 'interactive';
padding?: 'none' | 'sm' | 'md' | 'lg';
import React from "react";
import { Card as HeroUICard } from "@heroui/react";
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:
'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:
'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:
'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:
'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:
'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> = {
none: 'p-0',
sm: 'p-4',
md: 'p-6',
lg: 'p-8',
const paddingStyles: Record<CardPadding, string> = {
none: "p-0",
sm: "p-4",
md: "p-6",
lg: "p-8",
};
export const Card = React.forwardRef<HTMLDivElement, CardProps>(
(
{
variant = 'default',
padding = 'none',
className = '',
children,
...props
},
ref
) => {
return (
<div
ref={ref}
className={`
rounded-2xl overflow-hidden transition-colors
${variantStyles[variant]}
${paddingStyles[padding]}
${className}
`}
{...props}
>
{children}
</div>
);
}
);
export const Card = ({
variant = "default",
padding = "none",
className = "",
children,
...props
}: CardProps) => {
return (
<HeroUICard
{...props}
className={[
"rounded-2xl overflow-hidden transition-colors",
variantStyles[variant],
paddingStyles[padding],
className,
]
.filter(Boolean)
.join(" ")}
>
{children}
</HeroUICard>
);
};
Card.displayName = 'Card';
export interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
export interface CardHeaderProps
extends React.ComponentProps<typeof HeroUICard.Header> {
bordered?: boolean;
}
export const CardHeader: React.FC<CardHeaderProps> = ({
export const CardHeader = ({
bordered = false,
className = '',
className = "",
children,
...props
}) => {
}: CardHeaderProps) => {
return (
<div
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}
`}
<HeroUICard.Header
{...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}
</div>
</HeroUICard.Header>
);
};
export const CardTitle: React.FC<React.HTMLAttributes<HTMLHeadingElement>> = ({
className = '',
export const CardTitle = ({
className = "",
children,
...props
}) => {
}: React.ComponentProps<typeof HeroUICard.Title>) => {
return (
<h3
className={`text-lg sm:text-xl font-bold tracking-tight text-slate-900 dark:text-white transition-colors ${className}`}
<HeroUICard.Title
{...props}
className={[
"text-lg sm:text-xl font-bold tracking-tight",
"text-slate-900 dark:text-white",
"transition-colors",
className,
]
.filter(Boolean)
.join(" ")}
>
{children}
</h3>
</HeroUICard.Title>
);
};
export const CardDescription: React.FC<React.HTMLAttributes<HTMLParagraphElement>> = ({
className = '',
export const CardDescription = ({
className = "",
children,
...props
}) => {
}: React.ComponentProps<typeof HeroUICard.Description>) => {
return (
<p
className={`text-xs sm:text-sm text-slate-500 dark:text-slate-400 leading-relaxed ${className}`}
<HeroUICard.Description
{...props}
className={[
"text-xs sm:text-sm",
"text-slate-500 dark:text-slate-400",
"leading-relaxed",
className,
]
.filter(Boolean)
.join(" ")}
>
{children}
</p>
</HeroUICard.Description>
);
};
export const CardContent: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({
className = '',
export const CardContent = ({
className = "",
children,
...props
}) => {
}: React.ComponentProps<typeof HeroUICard.Content>) => {
return (
<div className={`p-5 sm:p-6 ${className}`} {...props}>
<HeroUICard.Content
{...props}
className={[
"p-5 sm:p-6",
className,
]
.filter(Boolean)
.join(" ")}
>
{children}
</div>
</HeroUICard.Content>
);
};
export interface CardFooterProps extends React.HTMLAttributes<HTMLDivElement> {
export interface CardFooterProps
extends React.ComponentProps<typeof HeroUICard.Footer> {
bordered?: boolean;
}
export const CardFooter: React.FC<CardFooterProps> = ({
export const CardFooter = ({
bordered = false,
className = '',
className = "",
children,
...props
}) => {
}: CardFooterProps) => {
return (
<div
className={`
p-5 sm:p-6 flex items-center justify-between
${bordered ? 'border-t border-slate-100 dark:border-slate-800/80' : ''}
${className}
`}
<HeroUICard.Footer
{...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}
</div>
</HeroUICard.Footer>
);
};
};
+132 -96
View File
@@ -1,112 +1,148 @@
"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;
description?: string;
errorMessage?: string;
checked?: boolean;
disabled?: boolean;
indeterminate?: boolean;
className?: string;
}
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
(
{
label,
description,
errorMessage,
indeterminate = false,
checked,
disabled,
className = '',
id,
name,
...props
},
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);
export const Checkbox = ({
label,
description,
errorMessage,
indeterminate = false,
checked,
isSelected,
disabled = false,
className = "",
id,
name,
onChange,
...props
}: CheckboxProps) => {
return (
<HeroUICheckbox
{...props}
id={id}
name={name}
isSelected={checked}
isDisabled={disabled}
isIndeterminate={indeterminate}
isInvalid={Boolean(errorMessage)}
onChange={onChange}
className={className}
>
<HeroUICheckbox.Content className="items-start gap-2.5">
<HeroUICheckbox.Control
className={[
"mt-0.5 shrink-0 size-4 rounded-md",
"border",
"transition-all duration-200",
useEffect(() => {
if (resolvedRef.current) {
resolvedRef.current.indeterminate = indeterminate;
}
}, [indeterminate, resolvedRef]);
// Estado normal
"bg-white dark:bg-slate-900",
"border-slate-300 dark:border-slate-700",
const isChecked = Boolean(checked);
// Hover
"group-hover:border-slate-400",
"dark:group-hover:border-slate-500",
return (
<div className={`flex flex-col space-y-1 ${className}`}>
<label
htmlFor={checkboxId}
className={`inline-flex items-start gap-2.5 select-none cursor-pointer group ${
disabled ? 'opacity-50 !cursor-not-allowed' : ''
}`}
// 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(" ")}
>
{/* 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}
/>
<HeroUICheckbox.Indicator>
{({ isSelected, isIndeterminate }) => {
if (isIndeterminate) {
return <FiMinus className="text-xs stroke-3" />;
}
<div
className={`
w-4 h-4 rounded-md flex items-center justify-center transition-all duration-200
border
peer-focus-visible:ring-2 peer-focus-visible:ring-offset-2
peer-focus-visible:ring-cyan-500 peer-focus-visible:ring-offset-white dark:peer-focus-visible:ring-offset-slate-950
${
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 ? (
<FiMinus className="text-xs stroke-3" />
) : isChecked ? (
<FiCheck className="text-xs stroke-3" />
) : null}
</div>
if (isSelected) {
return <FiCheck className="text-xs stroke-3" />;
}
return null;
}}
</HeroUICheckbox.Indicator>
</HeroUICheckbox.Control>
{(label || description) && (
<div className="flex flex-col text-left">
{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
"
>
{label}
</span>
)}
{description && (
<Description
className="
text-xs
text-slate-500 dark:text-slate-400
"
>
{description}
</Description>
)}
</div>
{/* Label and Sub-description */}
{(label || description) && (
<div className="flex flex-col text-left">
{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">
{label}
</span>
)}
{description && (
<span className="text-xs text-slate-500 dark:text-slate-400">
{description}
</span>
)}
</div>
)}
</label>
{errorMessage && (
<p className="text-xs text-rose-600 dark:text-rose-400 pl-6.5">{errorMessage}</p>
)}
</div>
);
}
);
</HeroUICheckbox.Content>
Checkbox.displayName = 'Checkbox';
{errorMessage && (
<FieldError className="pl-6.5 text-xs">
{errorMessage}
</FieldError>
)}
</HeroUICheckbox>
);
};
Checkbox.displayName = "Checkbox";
+333 -267
View File
@@ -1,289 +1,355 @@
"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;
value?: string; // "YYYY-MM-DD"
onChange: (value: string | undefined) => void;
value?: string; //Fecha en formato YYYY-MM-DD o undefined cuando se limpia
onChange: (value: string | undefined) => void; //Devuelve la fecha en formato YYYY-MM-DD o undefined cuando se limpia.
placeholder?: string;
required?: boolean;
maxDate?: Date;
maxDate?: Date; //Fecha máxima permitida.
className?: string;
errorMessage?: string;
}
const MONTHS = [
"Enero","Febrero","Marzo","Abril","Mayo","Junio",
"Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre",
];
const DAYS = ["Do","Lu","Ma","Mi","Ju","Vi","Sa"];
const DROPDOWN_H = 320; // px estimado del dropdown
function toCalendarDate(dateStr?: string): DateValue | undefined {
if (!dateStr) {
return undefined;
}
function parseDate(value?: string): Date | undefined {
if (!value) return undefined;
const d = new Date(value + "T12:00:00");
return isNaN(d.getTime()) ? undefined : d;
try {
return parseInternationalDate(dateStr);
} catch {
return undefined;
}
}
function formatDisplay(value?: string): string {
const d = parseDate(value);
if (!d) return "";
return d.toLocaleDateString("es-MX", { day: "numeric", month: "long", year: "numeric" });
function toDateString(date: DateValue): string {
const yyyy = date.year;
const mm = String(date.month).padStart(2, "0");
const dd = String(date.day).padStart(2, "0");
return `${yyyy}-${mm}-${dd}`;
}
export function DatePicker({
label,
value,
onChange,
placeholder = "Selecciona una fecha",
required,
maxDate = new Date(),
}: DatePickerProps) {
const selected = parseDate(value);
const [open, setOpen] = useState(false);
const [view, setView] = useState<"days" | "months" | "years">("days");
const [cursor, setCursor] = useState(() => {
const d = selected ?? new Date();
return { month: d.getMonth(), year: d.getFullYear() };
});
const [pos, setPos] = useState({ top: 0, left: 0, width: 0, openUp: false });
export const DatePicker = React.forwardRef<
HTMLDivElement,
DatePickerProps
>(
(
{
label,
value,
onChange,
placeholder,
required = false,
maxDate,
className = "",
errorMessage,
},
ref
) => {
const dateValue = useMemo(
() => toCalendarDate(value),
[value]
);
const triggerRef = useRef<HTMLButtonElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const maxDateValue = useMemo<DateValue | undefined>(() => {
if (!maxDate) {
return undefined;
}
const calcPos = useCallback(() => {
if (!triggerRef.current) return;
const r = triggerRef.current.getBoundingClientRect();
const spaceBelow = window.innerHeight - r.bottom;
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,
});
}, []);
try {
const yyyy = maxDate.getFullYear();
const mm = String(maxDate.getMonth() + 1).padStart(2, "0");
const dd = String(maxDate.getDate()).padStart(2, "0");
const openPicker = () => {
if (selected) setCursor({ month: selected.getMonth(), year: selected.getFullYear() });
setView("days");
calcPos();
setOpen(true);
};
return parseInternationalDate(`${yyyy}-${mm}-${dd}`);
} catch {
return undefined;
}
}, [maxDate]);
useEffect(() => {
if (!open) return;
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);
const handleChange = (newDate: DateValue | null) => {
if (newDate) {
onChange(toDateString(newDate));
} else {
onChange(undefined);
}
};
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);
const handleClear = () => {
onChange(undefined);
};
}, [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);
};
return (
<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 className="block text-xs font-semibold text-slate-700 dark:text-slate-300">
{label}
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 (
<div className="w-full space-y-1.5">
{label && (
<label className="block text-xs font-semibold text-slate-700 dark:text-slate-300">
{label}
{required && <span className="ml-1 text-rose-500 font-bold">*</span>}
</label>
)}
<div className="relative flex items-center">
<div className="absolute left-3.5 pointer-events-none text-slate-400 dark:text-slate-500">
<FiCalendar className="text-base" />
</div>
<button
ref={triggerRef}
type="button"
onClick={openPicker}
className={`
w-full pl-10 pr-10 py-2.5 text-sm rounded-xl text-left transition-all duration-200 outline-none
bg-white dark:bg-slate-900/90
border border-slate-300 dark:border-slate-800
hover:border-slate-400 dark:hover:border-slate-700
focus:border-cyan-500 focus:ring-4 focus:ring-cyan-500/15
dark:focus:border-cyan-500 dark:focus:ring-cyan-500/25
${value ? "text-slate-900 dark:text-slate-100" : "text-slate-400 dark:text-slate-500"}
`}
>
{value ? formatDisplay(value) : placeholder}
</button>
{value && (
<button
type="button"
onClick={() => onChange(undefined)}
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"
>
<FiX className="text-sm" />
</button>
{required && (
<span className="ml-1 font-bold text-rose-500">
*
</span>
)}
</Label>
)}
</div>
{typeof window !== "undefined" && createPortal(dropdown, document.body)}
</div>
);
}
{/* Campo de fecha */}
<HeroUIDateField.Group
className={[
"bg-white dark:bg-slate-900/90",
"border border-slate-300 dark:border-slate-800",
"hover:border-slate-400 dark:hover:border-slate-700",
"data-[focus-within=true]:border-cyan-500",
"data-[focus-within=true]:ring-4",
"data-[focus-within=true]:ring-cyan-500/15",
"rounded-xl flex items-center shadow-sm",
"transition-all duration-200 h-10",
errorMessage
? "border-rose-500 dark:border-rose-500"
: "",
].join(" ")}
>
{/* Icono izquierdo */}
<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 && (
<button
type="button"
onClick={handleClear}
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" />
</button>
)}
{/* 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>
</HeroUICalendar.Root>
</HeroUIDatePicker.Popover>
</HeroUIDatePicker.Root>
);
}
);
DatePicker.displayName = "DatePicker";
+162 -126
View File
@@ -1,6 +1,7 @@
"use client";
import React, { useState, useRef, useEffect } from "react";
import { FiCheck, FiChevronDown, FiSearch, FiX } from "react-icons/fi";
import { Popover as HeroUIPopover } from "@heroui/react";
import { useTheme } from "../../context/ThemeContext";
export interface DropdownOption {
@@ -27,7 +28,6 @@ export const Dropdown: React.FC<DropdownProps> = ({
const { isDark } = useTheme();
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");
const ref = useRef<HTMLDivElement>(null);
const searchRef = useRef<HTMLInputElement>(null);
const filtered = options.filter((o) =>
@@ -35,24 +35,18 @@ export const Dropdown: React.FC<DropdownProps> = ({
);
useEffect(() => {
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
setSearch("");
}
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, []);
useEffect(() => {
if (open) setTimeout(() => searchRef.current?.focus(), 50);
if (!open) {
setSearch("");
return;
}
const timer = setTimeout(() => searchRef.current?.focus(), 50);
return () => clearTimeout(timer);
}, [open]);
const toggle = (val: string) =>
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();
onChange(value.filter((v) => v !== val));
};
@@ -71,128 +65,170 @@ export const Dropdown: React.FC<DropdownProps> = ({
const divider = isDark ? "bg-slate-700" : "bg-slate-100";
return (
<div ref={ref} className={`relative ${className}`}>
{/* Trigger */}
<div
onClick={() => setOpen((p) => !p)}
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} ${
open ? "rounded-t-xl border-b-0" : "rounded-xl"
}`}
>
<div className="flex-1 flex items-center gap-1.5 min-w-0 overflow-hidden">
{value.length === 0 ? (
<span className={`text-sm font-medium ${isDark ? "text-slate-500" : "text-slate-400"}`}>
{placeholder}
</span>
) : (
<>
{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 ${
isDark ? "bg-violet-600/25 text-violet-300" : "bg-violet-100 text-violet-700"
}`}>
{labelOf(v)}
<button type="button" onClick={(e) => removeTag(v, e)} className="hover:opacity-70">
<FiX className="text-[10px]" />
</button>
</span>
))}
{value.length > 1 && (
<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"
}`}>
+{value.length - 2} más
<div className={`relative ${className}`}>
<HeroUIPopover.Root isOpen={open} onOpenChange={setOpen}>
<HeroUIPopover.Trigger className="block w-full">
<button
type="button"
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 rounded-b-none! border-b-0!" : "rounded-xl"
}`}
>
<div className="flex-1 flex items-center gap-1.5 min-w-0 overflow-hidden">
{value.length === 0 ? (
<span className={`text-sm font-medium ${isDark ? "text-slate-500" : "text-slate-400"}`}>
{placeholder}
</span>
) : (
<>
{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 ${
isDark ? "bg-violet-600/25 text-violet-300" : "bg-violet-100 text-violet-700"
}`}
>
{labelOf(v)}
<div
onClick={(e) => removeTag(v, e as any)}
className="hover:opacity-70 ml-1 cursor-pointer"
>
<FiX className="text-[10px]" />
</div>
</span>
))}
{value.length > 1 && (
<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"
}`}
>
+{value.length - 1} más
</span>
)}
</>
)}
</>
)}
</div>
<FiChevronDown className={`shrink-0 text-sm transition-transform duration-200 ${open ? "rotate-180" : ""} ${isDark ? "text-slate-500" : "text-slate-400"}`} />
</div>
{/* Menu — absolute, sin border-top para no doblar la línea */}
{open && (
<div className={`absolute left-0 top-full z-50 w-full rounded-b-xl border border-t-0 shadow-lg overflow-hidden ${bg} ${borderColor}`}>
{/* Search */}
<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"}`}>
<FiSearch className={`shrink-0 text-sm ${isDark ? "text-slate-500" : "text-slate-400"}`} />
<input
ref={searchRef}
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Buscar..."
className={`flex-1 text-sm bg-transparent outline-none ${
isDark ? "text-slate-200 placeholder:text-slate-600" : "text-slate-700 placeholder:text-slate-400"
}`}
</div>
<FiChevronDown
className={`shrink-0 text-sm transition-transform duration-200 ${
open ? "rotate-180" : ""
} ${isDark ? "text-slate-500" : "text-slate-400"}`}
/>
{search && (
<button onClick={() => setSearch("")} className={isDark ? "text-slate-500 hover:text-slate-300" : "text-slate-400 hover:text-slate-600"}>
<FiX className="text-xs" />
</button>
)}
</div>
</button>
</HeroUIPopover.Trigger>
<div className={`h-px ${divider}`} />
<HeroUIPopover.Content
offset={0}
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!`}
>
<HeroUIPopover.Dialog className="w-full flex flex-col p-0 outline-none">
<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"
}`}
>
<FiSearch className={`shrink-0 text-sm ${isDark ? "text-slate-500" : "text-slate-400"}`} />
<input
ref={searchRef}
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Buscar..."
className={`flex-1 text-sm bg-transparent outline-none ${
isDark ? "text-slate-200 placeholder:text-slate-600" : "text-slate-700 placeholder:text-slate-400"
}`}
/>
{search && (
<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" />
</button>
)}
</div>
{/* Options */}
<div className="max-h-52 overflow-y-auto py-1">
{filtered.length === 0 ? (
<p className={`px-4 py-3 text-xs text-center ${isDark ? "text-slate-600" : "text-slate-400"}`}>
Sin resultados
</p>
) : (
filtered.map((opt) => {
const isActive = value.includes(opt.value);
return (
<div className={`h-px ${divider}`} />
<div className="max-h-52 overflow-y-auto py-1 w-full">
{filtered.length === 0 ? (
<p className={`px-4 py-3 text-xs text-center ${isDark ? "text-slate-600" : "text-slate-400"}`}>
Sin resultados
</p>
) : (
filtered.map((opt) => {
const isActive = value.includes(opt.value);
return (
<button
key={opt.value}
type="button"
onClick={() => toggle(opt.value)}
className={`w-full flex items-center gap-3 px-4 py-3 text-sm transition-colors text-left outline-none ${
isActive
? isDark
? "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"
}`}
>
{isActive && <FiCheck className="text-white text-[10px]" />}
</span>
<span className="flex-1 font-medium">{opt.label}</span>
{opt.count !== undefined && (
<span
className={`text-[11px] px-1.5 py-0.5 rounded font-mono shrink-0 ${
isActive
? isDark
? "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}
</span>
)}
</button>
);
})
)}
</div>
{value.length > 0 && (
<>
<div className={`h-px w-full ${divider}`} />
<div className="px-4 py-2.5 flex justify-end w-full">
<button
key={opt.value}
type="button"
onClick={() => toggle(opt.value)}
className={`w-full flex items-center gap-3 px-4 py-3 text-sm transition-colors text-left ${
isActive
? isDark ? "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"
onClick={() => onChange([])}
className={`text-xs font-medium transition-colors outline-none ${
isDark ? "text-slate-500 hover:text-slate-300" : "text-slate-400 hover:text-slate-600"
}`}
>
<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]" />}
</span>
<span className="flex-1 font-medium">{opt.label}</span>
{opt.count !== undefined && (
<span className={`text-[11px] px-1.5 py-0.5 rounded font-mono shrink-0 ${
isActive
? isDark ? "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}
</span>
)}
Limpiar selección
</button>
);
})
</div>
</>
)}
</div>
{value.length > 0 && (
<>
<div className={`h-px ${divider}`} />
<div className="px-4 py-2.5 flex justify-end">
<button
onClick={() => onChange([])}
className={`text-xs font-medium transition-colors ${isDark ? "text-slate-500 hover:text-slate-300" : "text-slate-400 hover:text-slate-600"}`}
>
Limpiar selección
</button>
</div>
</>
)}
</div>
)}
</HeroUIPopover.Dialog>
</HeroUIPopover.Content>
</HeroUIPopover.Root>
</div>
);
};
};
+142 -28
View File
@@ -1,6 +1,8 @@
"use client";
import { useRef, useState } from "react";
import { FiUpload, FiX } from "react-icons/fi";
import { Button, CloseButton, Label } from "@heroui/react";
import { useTheme } from "@/context/ThemeContext";
export interface ImageUploadProps {
@@ -11,8 +13,15 @@ export interface ImageUploadProps {
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 inputRef = useRef<HTMLInputElement>(null);
const [dragging, setDragging] = useState(false);
@@ -20,34 +29,118 @@ export function ImageUpload({ label, value, onChange, aspectRatio = "square", hi
const handleFile = (file: File) => {
if (!file.type.startsWith("image/")) return;
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);
};
const handleDrop = (e: React.DragEvent) => {
e.preventDefault();
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
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 bg = isDark ? "bg-slate-800/50" : "bg-slate-50";
const active = dragging ? (isDark ? "border-violet-500 bg-violet-500/10" : "border-violet-400 bg-violet-50") : "";
const muted = isDark ? "text-slate-400" : "text-slate-500";
const borderColor = isDark
? "border-slate-700"
: "border-slate-200";
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 (
<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>
<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"}`}
onClick={() => inputRef.current?.click()}
onDragOver={(e) => { e.preventDefault(); setDragging(true); }}
onDragLeave={() => setDragging(false)}
role="button"
tabIndex={0}
aria-label={`Seleccionar imagen para ${label}`}
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}
>
{value ? (
@@ -55,34 +148,55 @@ export function ImageUpload({ label, value, onChange, aspectRatio = "square", hi
<img
src={value}
alt={label}
className="w-full h-full object-cover"
className="h-full w-full object-cover"
/>
<button
type="button"
onClick={(e) => { e.stopPropagation(); onChange(undefined); }}
className="absolute top-2 right-2 p-1 rounded-full bg-black/60 text-white hover:bg-black/80 transition"
<div
className="absolute right-2 top-2"
onClick={(event) => event.stopPropagation()}
>
<FiX className="text-xs" />
</button>
<CloseButton
aria-label="Eliminar imagen"
onPress={handleRemove}
className="rounded-full bg-black/60 text-white hover:bg-black/80"
>
<FiX className="text-xs" />
</CloseButton>
</div>
</>
) : (
<div className="flex flex-col items-center justify-center h-full gap-1.5 px-4 text-center">
<FiUpload className={`text-xl ${muted}`} />
<p className={`text-xs ${muted}`}>
<div className="flex h-full flex-col items-center justify-center gap-1.5 px-4 text-center">
<FiUpload className={`text-xl ${mutedColor}`} />
<p className={`text-xs ${mutedColor}`}>
{dragging ? "Suelta aquí" : "Click o arrastra"}
</p>
<Button
type="button"
variant="ghost"
size="sm"
onPress={openFilePicker}
className="mt-1"
>
Seleccionar
</Button>
</div>
)}
</div>
{hint && <p className={`text-[11px] mt-1 ${muted}`}>{hint}</p>}
{hint && (
<p className={`mt-1 text-[11px] ${mutedColor}`}>
{hint}
</p>
)}
<input
ref={inputRef}
type="file"
accept="image/*"
className="hidden"
onChange={(e) => { const f = e.target.files?.[0]; if (f) handleFile(f); }}
onChange={handleInputChange}
/>
</div>
);
+118 -124
View File
@@ -1,8 +1,9 @@
"use client";
import React, { useState } from 'react';
import { FiEye, FiEyeOff, FiX, FiAlertCircle } from 'react-icons/fi';
import React, { useState } from "react";
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;
helperText?: string;
errorMessage?: string;
@@ -10,28 +11,28 @@ export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElem
rightIcon?: React.ReactNode;
isClearable?: boolean;
onClear?: () => void;
sizeVariant?: 'sm' | 'md' | 'lg';
sizeVariant?: "sm" | "md" | "lg";
fullWidth?: boolean;
}
const sizeStyles = {
sm: {
input: 'py-2 text-xs rounded-lg',
icon: 'text-sm',
leftPad: 'pl-8',
rightPad: 'pr-8',
input: "py-2 text-xs rounded-lg",
icon: "text-sm",
leftPad: "pl-8",
rightPad: "pr-8",
},
md: {
input: 'py-2.5 text-sm rounded-xl',
icon: 'text-base',
leftPad: 'pl-10',
rightPad: 'pr-10',
input: "py-2.5 text-sm rounded-xl",
icon: "text-base",
leftPad: "pl-10",
rightPad: "pr-10",
},
lg: {
input: 'py-3.5 text-base rounded-2xl',
icon: 'text-lg',
leftPad: 'pl-12',
rightPad: 'pr-12',
input: "py-3.5 text-base rounded-2xl",
icon: "text-lg",
leftPad: "pl-12",
rightPad: "pr-12",
},
};
@@ -45,145 +46,138 @@ export const Input = React.forwardRef<HTMLInputElement, InputProps>(
rightIcon,
isClearable = false,
onClear,
sizeVariant = 'md',
sizeVariant = "md",
fullWidth = true,
type = 'text',
type = "text",
id,
name,
disabled,
required,
className = '',
className = "",
value,
...props
},
ref
ref,
) => {
const [showPassword, setShowPassword] = useState(false);
const inputId = id || name || (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined);
const isPasswordType = type === 'password';
const computedType = isPasswordType ? (showPassword ? 'text' : 'password') : type;
const inputId = id || name || (label ? label.toLowerCase().replace(/\s+/g, "-") : undefined);
const isPasswordType = type === "password";
const computedType = isPasswordType ? (showPassword ? "text" : "password") : type;
const hasError = Boolean(errorMessage);
const hasValue = value !== undefined && value !== '' && value !== null;
const hasValue = value !== undefined && value !== "" && value !== null;
const currentSize = sizeStyles[sizeVariant];
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 && (
<div className="flex items-center justify-between">
<label
htmlFor={inputId}
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>
<Label
htmlFor={inputId}
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}
</Label>
)}
{/* Input 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 Input Element */}
<input
ref={ref}
id={inputId}
name={name}
type={computedType}
value={value}
disabled={disabled}
required={required}
<div className="relative flex items-center w-full">
<InputGroup
className={`
w-full px-3.5 font-normal transition-all duration-200 outline-none
bg-white text-slate-900 placeholder:text-slate-400
border border-slate-300
hover:border-slate-400
focus:border-cyan-500 focus:ring-4 focus:ring-cyan-500/15
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}
`}
{...props}
/>
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} `}>
{/* Right Action Icons (Clear, Password Toggle, Custom RightIcon) */}
<div className="absolute right-3 flex items-center gap-1.5 text-slate-400 dark:text-slate-500">
{isClearable && hasValue && !disabled && (
<button
type="button"
tabIndex={-1}
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"
>
<FiX className="text-sm" />
</button>
{/* Left Icon */}
{leftIcon && (
<InputGroup.Prefix
className={`flex items-center justify-center pl-3.5 pr-2 shrink-0 pointer-events-none transition-colors ${
hasError ? "text-rose-500" : "text-slate-400 dark:text-slate-500"
} ${currentSize.icon}`}>
{leftIcon}
</InputGroup.Prefix>
)}
{isPasswordType && !disabled && (
<button
type="button"
tabIndex={-1}
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"
>
{showPassword ? (
<FiEyeOff className={currentSize.icon} />
) : (
<FiEye className={currentSize.icon} />
)}
</button>
)}
{/* Core Input Element */}
<HeroInput
ref={ref}
id={inputId}
name={name}
type={computedType}
value={value}
disabled={disabled}
required={required}
className={`
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
dark:text-slate-100 dark:placeholder:text-slate-500
${hasError ? "text-rose-900 dark:text-rose-200" : ""}
${!leftIcon ? "pl-3.5" : ""}
${!rightIcon && !isClearable && !isPasswordType ? "pr-3.5" : ""}
${className}
`}
{...props}
/>
{!isPasswordType && rightIcon && (
<div className={`pointer-events-none flex items-center justify-center ${currentSize.icon}`}>
{rightIcon}
</div>
)}
</div>
{/* Right Action Icons */}
<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 && (
<button
type="button"
tabIndex={-1}
onClick={onClear}
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" />
</button>
)}
{/* Password Toggle */}
{isPasswordType && !disabled && (
<button
type="button"
tabIndex={-1}
onClick={() => setShowPassword((prev) => !prev)}
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} />}
</button>
)}
{/* Custom Right Icon */}
{!isPasswordType && rightIcon && (
<div className={`pointer-events-none flex items-center justify-center ${currentSize.icon}`}>
{rightIcon}
</div>
)}
</InputGroup.Suffix>
</InputGroup>
</div>
{/* Error or Helper Message */}
{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" />
<span>{errorMessage}</span>
</p>
</FieldError>
) : 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}
</div>
</TextField>
);
}
},
);
Input.displayName = 'Input';
Input.displayName = "Input";
+68 -93
View File
@@ -1,13 +1,15 @@
"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 {
isOpen: boolean;
onClose: () => void;
title?: React.ReactNode;
description?: React.ReactNode;
size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full';
size?: "sm" | "md" | "lg" | "xl" | "2xl" | "full";
children: React.ReactNode;
footer?: React.ReactNode;
closeOnOverlayClick?: boolean;
@@ -16,12 +18,12 @@ export interface ModalProps {
}
const sizeStyles = {
sm: 'max-w-md',
md: 'max-w-lg',
lg: 'max-w-2xl',
xl: 'max-w-4xl',
'2xl': 'max-w-6xl',
full: 'max-w-[95vw] h-[90vh]',
sm: "max-w-md",
md: "max-w-lg",
lg: "max-w-2xl",
xl: "max-w-4xl",
"2xl": "max-w-6xl",
full: "max-w-[95vw] h-[90vh]",
};
export const Modal: React.FC<ModalProps> = ({
@@ -29,101 +31,74 @@ export const Modal: React.FC<ModalProps> = ({
onClose,
title,
description,
size = 'md',
size = "md",
children,
footer,
closeOnOverlayClick = true,
showCloseButton = true,
className = '',
className = "",
}) => {
// ESC key handler and body scroll lock
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
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();
<HeroModal>
<HeroModal.Backdrop
isOpen={isOpen}
isDismissable={closeOnOverlayClick}
onOpenChange={(open) => {
if (!open) {
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">
<HeroModal.Dialog
aria-modal="true"
className={`
relative w-full rounded-3xl overflow-hidden shadow-2xl z-10 p-0
bg-white text-slate-900 border border-slate-200
dark:bg-slate-900/95 dark:text-slate-100 dark:border-slate-800
transition-all transform animate-scaleUp
${sizeStyles[size]}
${className}
`}>
{/* Modal Dialog Card */}
<div
className={`
relative w-full rounded-3xl overflow-hidden shadow-2xl z-10
bg-white text-slate-900 border border-slate-200
dark:bg-slate-900/95 dark:text-slate-100 dark:border-slate-800
transition-all transform animate-scaleUp
${sizeStyles[size]}
${className}
`}
>
{/* Header */}
{(title || description || showCloseButton) && (
<div 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">
{title && (
<h2 className="text-xl font-bold text-slate-900 dark:text-white tracking-tight">
{title}
</h2>
)}
{description && (
<p className="text-xs sm:text-sm text-slate-500 dark:text-slate-400">
{description}
</p>
)}
</div>
{/* Header */}
{(title || description || showCloseButton) && (
<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">
{title && (
<HeroModal.Heading className="text-xl font-bold text-slate-900 dark:text-white tracking-tight">
{title}
</HeroModal.Heading>
)}
{description && (
<p className="text-xs sm:text-sm text-slate-500 dark:text-slate-400">{description}</p>
)}
</div>
{showCloseButton && (
<button
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"
>
<FiX className="text-lg" />
</button>
{showCloseButton && (
<HeroModal.CloseTrigger
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" />
</HeroModal.CloseTrigger>
)}
</HeroModal.Header>
)}
</div>
)}
{/* Body Content */}
<div className="p-6 overflow-y-auto max-h-[calc(85vh-160px)]">
{children}
</div>
{/* Body Content */}
<HeroModal.Body className="p-6 overflow-y-auto max-h-[calc(85vh-160px)]">{children}</HeroModal.Body>
{/* 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">
{footer}
</div>
)}
</div>
</div>
{/* Footer */}
{footer && (
<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}
</HeroModal.Footer>
)}
</HeroModal.Dialog>
</HeroModal.Container>
</HeroModal.Backdrop>
</HeroModal>
);
};
+140 -105
View File
@@ -1,6 +1,9 @@
"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 {
value: string | number;
@@ -8,158 +11,190 @@ export interface SelectOption {
disabled?: boolean;
}
export interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'size'> {
export interface SelectProps {
label?: string;
helperText?: string;
errorMessage?: string;
leftIcon?: React.ReactNode;
options?: SelectOption[];
sizeVariant?: 'sm' | 'md' | 'lg';
sizeVariant?: "sm" | "md" | "lg";
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 = {
sm: {
select: 'py-2 text-xs rounded-lg',
icon: 'text-sm',
leftPad: 'pl-8',
rightPad: 'pr-8',
select: "py-2 text-xs rounded-lg",
icon: "text-sm",
leftPad: "pl-8",
rightPad: "pr-8",
},
md: {
select: 'py-2.5 text-sm rounded-xl',
icon: 'text-base',
leftPad: 'pl-10',
rightPad: 'pr-10',
select: "py-2.5 text-sm rounded-xl",
icon: "text-base",
leftPad: "pl-10",
rightPad: "pr-10",
},
lg: {
select: 'py-3.5 text-base rounded-2xl',
icon: 'text-lg',
leftPad: 'pl-12',
rightPad: 'pr-12',
select: "py-3.5 text-base rounded-2xl",
icon: "text-lg",
leftPad: "pl-12",
rightPad: "pr-12",
},
};
export const Select = React.forwardRef<HTMLSelectElement, SelectProps>(
export const Select = React.forwardRef<HTMLDivElement, SelectProps>(
(
{
label,
helperText,
errorMessage,
leftIcon,
options,
sizeVariant = 'md',
options = [],
sizeVariant = "md",
fullWidth = true,
id,
name,
disabled,
required,
className = '',
className = "",
value,
defaultValue,
onChange,
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 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 (
<div className={`${fullWidth ? 'w-full' : 'inline-block'} space-y-1.5`}>
{/* Label */}
{label && (
<div className="flex items-center justify-between">
<label
htmlFor={selectId}
<div className={`${fullWidth ? "w-full" : "inline-block"} space-y-1.5`}>
<HeroSelect
ref={ref}
id={selectId}
name={name}
selectedKey={selectedKey}
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'
}`}
>
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>
</Label>
)}
{/* Core Select Element */}
<select
ref={ref}
id={selectId}
name={name}
disabled={disabled}
required={required}
<HeroSelect.Trigger
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
border border-slate-300
hover:border-slate-400
focus:border-cyan-500 focus:ring-4 focus:ring-cyan-500/15
dark:bg-slate-900/90 dark:text-slate-100
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 : ''}
${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'
: ''
}
focus:border-cyan-500
focus:ring-4
focus:ring-cyan-500/15
dark:bg-slate-900/90
dark:text-slate-100
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
${currentSize.select}
${leftIcon ? currentSize.leftPad : ""}
${currentSize.rightPad}
${hasError ? "border-rose-500! ring-rose-500/20! text-rose-900 dark:text-rose-200" : ""}
${className}
`}
{...props}
>
{options
? options.map((opt) => (
<option
key={opt.value}
value={opt.value}
disabled={opt.disabled}
className="bg-white text-slate-900 dark:bg-slate-900 dark:text-slate-100"
>
{opt.label}
</option>
))
: children}
</select>
`}>
{leftIcon && (
<span
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}
</span>
)}
{/* Right Chevron Icon */}
<div
className={`absolute right-3.5 pointer-events-none flex items-center justify-center text-slate-400 dark:text-slate-500 ${currentSize.icon}`}
>
<FiChevronDown />
</div>
</div>
<HeroSelect.Value className="w-full text-left" />
{/* Error or Helper Message */}
{hasError ? (
<p className="flex items-center gap-1.5 text-xs text-rose-600 dark:text-rose-400 animate-fadeIn">
<FiAlertCircle className="shrink-0 text-xs" />
<span>{errorMessage}</span>
</p>
) : helperText ? (
<p className="text-xs text-slate-500 dark:text-slate-400">{helperText}</p>
) : null}
<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 />
</HeroSelect.Indicator>
</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>
{hasError ? (
<FieldError className="flex items-center gap-1.5 text-xs text-rose-600 dark:text-rose-400">
<FiAlertCircle className="shrink-0 text-xs" />
<span>{errorMessage}</span>
</FieldError>
) : helperText ? (
<Description className="text-xs text-slate-500 dark:text-slate-400">{helperText}</Description>
) : null}
</HeroSelect>
</div>
);
}
},
);
Select.displayName = 'Select';
Select.displayName = "Select";
+23 -27
View File
@@ -1,45 +1,41 @@
"use client";
import React from 'react';
import React from "react";
import { Spinner as HeroSpinner } from "@heroui/react";
export interface SpinnerProps {
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
color?: 'cyan' | 'blue' | 'white' | 'slate' | 'current';
size?: "xs" | "sm" | "md" | "lg" | "xl";
color?: "cyan" | "blue" | "white" | "slate" | "current";
className?: string;
label?: string;
}
const sizeClasses = {
xs: 'w-3.5 h-3.5 border-2',
sm: 'w-4 h-4 border-2',
md: 'w-6 h-6 border-2',
lg: 'w-8 h-8 border-3',
xl: 'w-12 h-12 border-4',
xs: "w-3.5 h-3.5",
sm: "w-4 h-4",
md: "w-6 h-6",
lg: "w-8 h-8",
xl: "w-12 h-12",
};
const colorClasses = {
cyan: 'border-cyan-500/20 border-t-cyan-500 dark:border-cyan-400/20 dark:border-t-cyan-400',
blue: 'border-blue-500/20 border-t-blue-500 dark:border-blue-400/20 dark:border-t-blue-400',
white: 'border-white/30 border-t-white',
slate: 'border-slate-300 border-t-slate-700 dark:border-slate-700 dark:border-t-slate-200',
current: 'border-current/20 border-t-current',
cyan: "text-cyan-500 dark:text-cyan-400",
blue: "text-blue-500 dark:text-blue-400",
white: "text-white",
slate: "text-slate-700 dark:text-slate-200",
current: "text-current",
};
export const Spinner: React.FC<SpinnerProps> = ({
size = 'md',
color = 'cyan',
className = '',
label = 'Cargando...',
size = "md",
color = "cyan",
className = "",
label = "Cargando...",
}) => {
return (
<div
role="status"
<HeroSpinner
aria-label={label}
className={`inline-flex items-center justify-center ${className}`}
>
<div
className={`rounded-full animate-spin ${sizeClasses[size]} ${colorClasses[color]}`}
/>
<span className="sr-only">{label}</span>
</div>
className={`inline-block animate-spin ${sizeClasses[size]} ${colorClasses[color]} ${className}`}
/>
);
};
};
+102 -81
View File
@@ -1,5 +1,11 @@
"use client";
import React from 'react';
import React from "react";
import {
Switch as HeroSwitch,
Label,
Description,
} from "@heroui/react";
export interface SwitchProps {
label?: React.ReactNode;
@@ -7,7 +13,7 @@ export interface SwitchProps {
checked?: boolean;
onChange?: (checked: boolean) => void;
disabled?: boolean;
size?: 'sm' | 'md' | 'lg';
size?: "sm" | "md" | "lg";
checkedIcon?: React.ReactNode;
uncheckedIcon?: React.ReactNode;
id?: string;
@@ -16,21 +22,21 @@ export interface SwitchProps {
}
const trackSizes = {
sm: 'w-8 h-4.5',
md: 'w-11 h-6',
lg: 'w-14 h-7.5',
sm: "w-8 h-[18px]",
md: "w-11 h-6",
lg: "w-14 h-[30px]",
};
const thumbSizes = {
sm: 'w-3.5 h-3.5',
md: 'w-5 h-5',
lg: 'w-6.5 h-6.5',
sm: "size-3.5",
md: "size-5",
lg: "size-[26px]",
};
const translateSizes = {
sm: 'translate-x-3.5',
md: 'translate-x-5',
lg: 'translate-x-6.5',
sm: "translate-x-3.5",
md: "translate-x-5",
lg: "translate-x-[26px]",
};
export const Switch: React.FC<SwitchProps> = ({
@@ -39,85 +45,100 @@ export const Switch: React.FC<SwitchProps> = ({
checked = false,
onChange,
disabled = false,
size = 'md',
size = "md",
checkedIcon,
uncheckedIcon,
id,
name,
className = '',
className = "",
}) => {
const switchId = id || name || (typeof label === 'string' ? label.toLowerCase().replace(/\s+/g, '-') : undefined);
const handleClick = () => {
if (!disabled && onChange) {
onChange(!checked);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault();
handleClick();
}
};
const switchId =
id ||
name ||
(typeof label === "string"
? label.toLowerCase().replace(/\s+/g, "-")
: undefined);
return (
<div className={`inline-flex items-start gap-3 select-none ${className}`}>
{/* Switch Button */}
<button
id={switchId}
type="button"
role="switch"
aria-checked={checked}
disabled={disabled}
onClick={handleClick}
onKeyDown={handleKeyDown}
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]}
${
checked
? 'bg-cyan-500 dark:bg-cyan-500'
: 'bg-slate-300 dark:bg-slate-700 hover:bg-slate-400 dark:hover:bg-slate-600'
}
`}
>
{/* Thumb */}
<span
<HeroSwitch
id={switchId}
name={name}
isSelected={checked}
isDisabled={disabled}
size={size}
className={className}
onChange={(e: any) => {
if (!onChange || disabled) return;
const val = typeof e === "boolean" ? e : Boolean(e?.target?.checked);
onChange(val);
}}
>
<HeroSwitch.Content>
<HeroSwitch.Control
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
${thumbSizes[size]}
${checked ? translateSizes[size] : 'translate-x-0'}
${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
? "bg-cyan-500"
: "bg-slate-300 hover:bg-slate-400 dark:bg-slate-700 dark:hover:bg-slate-600"
}
${disabled ? "cursor-not-allowed opacity-50" : ""}
`}
>
{checked ? checkedIcon : uncheckedIcon}
</span>
</button>
<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>
{/* 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}
</span>
)}
</div>
)}
</div>
{/* 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"
}
`}
>
{label}
</Label>
)}
{description && (
<Description
className={`
text-xs text-slate-500 dark:text-slate-400 cursor-pointer
${disabled ? "cursor-not-allowed" : ""}
`}
>
{description}
</Description>
)}
</div>
)}
</HeroSwitch.Content>
</HeroSwitch>
);
};
};