diff --git a/package-lock.json b/package-lock.json index 8d3cb015..780dd381 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "frappe-ui-react", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "frappe-ui-react", - "version": "1.0.1", + "version": "1.0.2", "workspaces": [ "packages/*" ], @@ -132,7 +132,6 @@ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -1767,7 +1766,6 @@ "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -1821,6 +1819,60 @@ "node": ">=6.9.0" } }, + "node_modules/@base-ui/react": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@base-ui/react/-/react-1.0.0.tgz", + "integrity": "sha512-4USBWz++DUSLTuIYpbYkSgy1F9ZmNG9S/lXvlUN6qMK0P0RlW+6eQmDUB4DgZ7HVvtXl4pvi4z5J2fv6Z3+9hg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@base-ui/utils": "0.2.3", + "@floating-ui/react-dom": "^2.1.6", + "@floating-ui/utils": "^0.2.10", + "reselect": "^5.1.1", + "tabbable": "^6.3.0", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17 || ^18 || ^19", + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@base-ui/utils": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.2.3.tgz", + "integrity": "sha512-/CguQ2PDaOzeVOkllQR8nocJ0FFIDqsWIcURsVmm53QGo8NhFNpePjNlyPIB41luxfOqnG7PU0xicMEw3ls7XQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "@floating-ui/utils": "^0.2.10", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "@types/react": "^17 || ^18 || ^19", + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", @@ -1940,7 +1992,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1964,7 +2015,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -3411,666 +3461,6 @@ "url": "https://opencollective.com/popperjs" } }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "license": "MIT" - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "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.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "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-dialog": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", - "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "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-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", - "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "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-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "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-toast": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", - "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", - "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "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-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "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/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "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-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "license": "MIT" - }, "node_modules/@react-aria/focus": { "version": "3.21.2", "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.2.tgz", @@ -5301,7 +4691,6 @@ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -5581,7 +4970,6 @@ "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -5590,9 +4978,8 @@ "version": "19.2.1", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.1.tgz", "integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==", - "devOptional": true, + "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -5656,7 +5043,6 @@ "integrity": "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.46.0", @@ -5697,7 +5083,6 @@ "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.0", "@typescript-eslint/types": "8.46.0", @@ -6222,7 +5607,6 @@ "integrity": "sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@testing-library/dom": "^10.4.0", "@testing-library/user-event": "^14.6.1", @@ -6360,7 +5744,6 @@ "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", @@ -6419,7 +5802,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -6546,18 +5928,6 @@ "sprintf-js": "~1.0.2" } }, - "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/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -6952,7 +6322,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -7766,12 +7135,6 @@ "node": ">=8" } }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "license": "MIT" - }, "node_modules/diff-sequences": { "version": "29.6.3", "dev": true, @@ -8077,7 +7440,6 @@ "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -8119,7 +7481,6 @@ "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "debug": "^4.3.4" }, @@ -8156,7 +7517,6 @@ "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8860,15 +8220,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/get-package-type": { "version": "0.1.0", "dev": true, @@ -9924,7 +9275,6 @@ "version": "30.2.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -10854,7 +10204,6 @@ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", @@ -12693,7 +12042,6 @@ "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin-prettier.js" }, @@ -12812,7 +12160,6 @@ "node_modules/quill": { "version": "2.0.3", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "eventemitter3": "^5.0.1", "lodash-es": "^4.17.21", @@ -12826,7 +12173,6 @@ "node_modules/quill-delta": { "version": "5.1.0", "license": "MIT", - "peer": true, "dependencies": { "fast-diff": "^1.3.0", "lodash.clonedeep": "^4.5.0", @@ -12905,7 +12251,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -12946,7 +12291,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -13016,53 +12360,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-remove-scroll": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", - "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/react-resizable": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz", @@ -13076,28 +12373,6 @@ "react": ">= 16.3" } }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", @@ -13256,6 +12531,12 @@ "node": ">=0.10.0" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", @@ -13374,7 +12655,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.4.tgz", "integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -13911,7 +13191,6 @@ "integrity": "sha512-kfr6kxQAjA96ADlH6FMALJwJ+eM80UqXy106yVHNgdsAP/CdzkkicglRAhZAvUycXK9AeadF6KZ00CWLtVMN4w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@storybook/global": "^5.0.0", "@testing-library/jest-dom": "^6.6.3", @@ -14330,9 +13609,9 @@ } }, "node_modules/tabbable": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", "license": "MIT" }, "node_modules/tailwindcss": { @@ -14719,7 +13998,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -14925,49 +14203,6 @@ "punycode": "^2.1.0" } }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -15006,7 +14241,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz", "integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -15148,7 +14382,6 @@ "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", @@ -15694,16 +14927,13 @@ }, "packages/frappe-ui-react": { "name": "@rtcamp/frappe-ui-react", - "version": "1.0.1", + "version": "1.0.2", "license": "MIT", "dependencies": { + "@base-ui/react": "^1.0.0", "@floating-ui/react": "^0.27.13", "@headlessui/react": "^2.2.6", "@popperjs/core": "^2.11.8", - "@radix-ui/react-dialog": "^1.1.14", - "@radix-ui/react-dropdown-menu": "^2.1.15", - "@radix-ui/react-toast": "^1.2.15", - "@radix-ui/react-tooltip": "^1.2.7", "@tailwindcss/vite": "^4.1.11", "clsx": "^2.1.1", "dayjs": "^1.11.13", diff --git a/packages/frappe-ui-react/package.json b/packages/frappe-ui-react/package.json index fd279ba5..8e6b7ecd 100644 --- a/packages/frappe-ui-react/package.json +++ b/packages/frappe-ui-react/package.json @@ -38,13 +38,10 @@ "publish:remote": "npm publish" }, "dependencies": { + "@base-ui/react": "^1.0.0", "@floating-ui/react": "^0.27.13", "@headlessui/react": "^2.2.6", "@popperjs/core": "^2.11.8", - "@radix-ui/react-dialog": "^1.1.14", - "@radix-ui/react-dropdown-menu": "^2.1.15", - "@radix-ui/react-toast": "^1.2.15", - "@radix-ui/react-tooltip": "^1.2.7", "@tailwindcss/vite": "^4.1.11", "clsx": "^2.1.1", "dayjs": "^1.11.13", diff --git a/packages/frappe-ui-react/src/components/dialog/dialog.css b/packages/frappe-ui-react/src/components/dialog/dialog.css index 1d75aab6..6970a1aa 100644 --- a/packages/frappe-ui-react/src/components/dialog/dialog.css +++ b/packages/frappe-ui-react/src/components/dialog/dialog.css @@ -1,4 +1,4 @@ -@keyframes dialog-overlay-in { +@keyframes dialog-backdrop-in { from { opacity: 0; } @@ -7,7 +7,7 @@ } } -@keyframes dialog-overlay-out { +@keyframes dialog-backdrop-out { from { opacity: 1; } @@ -38,18 +38,18 @@ } } -.dialog-overlay[data-state='open'] { - animation: dialog-overlay-in 100ms ease-out; +.dialog-backdrop[data-open] { + animation: dialog-backdrop-in 100ms ease-out; } -.dialog-overlay[data-state='closed'] { - animation: dialog-overlay-out 150ms ease-in; +.dialog-backdrop[data-closed] { + animation: dialog-backdrop-out 150ms ease-in; } -.dialog-content[data-state='open'] { +.dialog-content[data-open] { animation: dialog-content-in 100ms ease-out; } -.dialog-content[data-state='closed'] { +.dialog-content[data-closed] { animation: dialog-content-out 150ms ease-in; -} \ No newline at end of file +} diff --git a/packages/frappe-ui-react/src/components/dialog/dialog.stories.tsx b/packages/frappe-ui-react/src/components/dialog/dialog.stories.tsx index c79b7934..34deeb11 100644 --- a/packages/frappe-ui-react/src/components/dialog/dialog.stories.tsx +++ b/packages/frappe-ui-react/src/components/dialog/dialog.stories.tsx @@ -286,8 +286,8 @@ export const WithInteractiveComponents: Story = { setAutocompleteValue(_value as AutocompleteOption) } /> -
-
@@ -209,11 +204,11 @@ const Dialog = ({ )} )} - - - - - + + + + + ); }; diff --git a/packages/frappe-ui-react/src/components/dialog/types.ts b/packages/frappe-ui-react/src/components/dialog/types.ts index 36da537e..2f423b48 100644 --- a/packages/frappe-ui-react/src/components/dialog/types.ts +++ b/packages/frappe-ui-react/src/components/dialog/types.ts @@ -15,7 +15,7 @@ export interface DialogActionButtonProps { } export interface DialogOptions { - title?: (() => React.ReactNode) | string; + title?: (() => React.ReactElement) | string; message?: string; size?: | "xs" diff --git a/packages/frappe-ui-react/src/components/dropdown/dropdown.tsx b/packages/frappe-ui-react/src/components/dropdown/dropdown.tsx index 7b8d753f..823e4fde 100644 --- a/packages/frappe-ui-react/src/components/dropdown/dropdown.tsx +++ b/packages/frappe-ui-react/src/components/dropdown/dropdown.tsx @@ -1,5 +1,5 @@ import React, { useMemo, useCallback } from "react"; -import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; +import { Menu } from "@base-ui/react/menu"; import { Button, ButtonProps } from "../button"; import { Switch } from "../switch"; @@ -10,10 +10,11 @@ import type { DropdownOptions, } from "./types"; import FeatherIcon, { type FeatherIconProps } from "../featherIcon"; +import clsx from "clsx"; const cssClasses = { dropdownContent: - "min-w-40 divide-y divide-outline-gray-modals rounded-lg bg-surface-modal shadow-2xl ring-black focus:outline-none dropdown-content border border-outline-gray-1 z-100", + "min-w-40 divide-y divide-outline-gray-modals rounded-lg bg-surface-modal shadow-2xl ring-black focus:outline-none dropdown-content border border-outline-gray-1", groupContainer: "p-1.5", groupLabel: "flex h-7 items-center px-2 text-sm font-medium text-ink-gray-7", itemLabel: "whitespace-nowrap", @@ -23,6 +24,7 @@ const cssClasses = { "group flex h-7 w-full items-center rounded px-2 text-base focus:outline-none", submenuTrigger: "group flex h-7 w-full items-center rounded px-2 text-base text-ink-gray-6 focus:outline-none", + dropdownPositioner: "z-100", }; const Dropdown: React.FC = ({ @@ -180,64 +182,70 @@ const Dropdown: React.FC = ({ ); } else if (item.submenu) { return ( - - - - - - - {processOptionsIntoGroups(item.submenu).map((submenuGroup) => ( -
- {submenuGroup.group && !submenuGroup.hideLabel && ( - - {submenuGroup.group} - - )} - {submenuGroup.items.map((subItem) => ( - subItem.onClick?.()} - > - {renderDropdownItem(subItem)} - - ))} -
- ))} -
-
-
+ )} + className={clsx( + cssClasses.submenuTrigger, + getSubmenuBackgroundColor(item) + )} + > + {item.label} + + } + nativeButton={true} + /> + + + + {processOptionsIntoGroups(item.submenu).map((submenuGroup) => ( + + {submenuGroup.group && !submenuGroup.hideLabel && ( + + {submenuGroup.group} + + )} + {submenuGroup.items.map((subItem) => ( + subItem.onClick?.()} + render={renderDropdownItem(subItem)} + nativeButton={ + !subItem.switch && + !subItem.submenu && + !subItem.component + } + /> + ))} + + ))} + + + + ); } else { return ( @@ -245,6 +253,7 @@ const Dropdown: React.FC = ({ className={`${cssClasses.itemButton} ${getTextColor( item )} ${getSubmenuBackgroundColor(item)}`} + data-testid="dropdown-item-button" > {item.icon && (typeof item.icon === "string" ? ( @@ -262,49 +271,53 @@ const Dropdown: React.FC = ({ }; return ( - - - {children ? ( - React.cloneElement(children as React.ReactElement, { ...attrs }) - ) : ( - - )} - + + + {button?.label || "Options"} + + ) + } + /> - - - {groups.map((group) => ( -
- {group.group && !group.hideLabel && ( - - {group.group} - - )} - {group.items.map((item) => ( -
- !item.switch && item.onClick?.()}> - {renderDropdownItem(item)} - -
- ))} -
- ))} -
-
-
+ + + + {groups.map((group) => ( + + {group.group && !group.hideLabel && ( + + {group.group} + + )} + {group.items.map((item) => ( +
+ !item.switch && item.onClick?.()} + render={renderDropdownItem(item)} + nativeButton={ + !item.switch && !item.submenu && !item.component + } + /> +
+ ))} +
+ ))} +
+
+
+ ); }; diff --git a/packages/frappe-ui-react/src/components/dropdown/tests/dropdown.tsx b/packages/frappe-ui-react/src/components/dropdown/tests/dropdown.tsx new file mode 100644 index 00000000..590e5ffb --- /dev/null +++ b/packages/frappe-ui-react/src/components/dropdown/tests/dropdown.tsx @@ -0,0 +1,368 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import userEvent from "@testing-library/user-event"; + +import Dropdown from "../dropdown"; + +describe("Dropdown", () => { + it("renders dropdown with given items", async () => { + const user = userEvent.setup(); + const items = [ + { label: "Item 1", onClick: jest.fn() }, + { label: "Item 2", onClick: jest.fn() }, + ]; + + render(); + + const trigger = screen.getByTestId("dropdown-trigger"); + await user.click(trigger); + + // Wait for menu to open + expect(await screen.findByText("Item 1")).toBeInTheDocument(); + + const menuItems = screen.getAllByTestId("dropdown-item-button"); + expect(menuItems).toHaveLength(2); + expect(menuItems[0]).toHaveTextContent("Item 1"); + expect(menuItems[1]).toHaveTextContent("Item 2"); + }); + + it("calls onClick handler when an item is clicked", async () => { + const user = userEvent.setup(); + const onClickMock = jest.fn(); + const items = [{ label: "Clickable Item", onClick: onClickMock }]; + + render(); + + await user.click(screen.getByTestId("dropdown-trigger")); + + expect(await screen.findByText("Clickable Item")).toBeInTheDocument(); + + await user.click(screen.getByTestId("dropdown-item-button")); + + expect(onClickMock).toHaveBeenCalledTimes(1); + }); + + it("renders submenu items correctly", async () => { + const user = userEvent.setup(); + const items = [ + { + label: "Parent Item", + submenu: [ + { label: "Sub Item 1", onClick: jest.fn() }, + { label: "Sub Item 2", onClick: jest.fn() }, + ], + }, + ]; + + render(); + + await user.click(screen.getByTestId("dropdown-trigger")); + + expect(await screen.findByText("Parent Item")).toBeInTheDocument(); + + const submenuTrigger = screen.getByTestId("dropdown-submenu-trigger"); + await user.click(submenuTrigger); + + expect(await screen.findByText("Sub Item 1")).toBeInTheDocument(); + expect(screen.getByText("Sub Item 2")).toBeInTheDocument(); + }); + + it("renders default button label when no button prop provided", () => { + render(); + + expect(screen.getByTestId("dropdown-trigger")).toHaveTextContent("Options"); + }); + + it("renders custom button label when button prop provided", () => { + render(); + + expect(screen.getByTestId("dropdown-trigger")).toHaveTextContent( + "Custom Label" + ); + }); + + it("renders custom children as trigger", async () => { + const user = userEvent.setup(); + const items = [{ label: "Item 1", onClick: jest.fn() }]; + + render( + + + + ); + + const customTrigger = screen.getByTestId("custom-trigger"); + expect(customTrigger).toHaveTextContent("Custom Trigger"); + + await user.click(customTrigger); + expect(await screen.findByText("Item 1")).toBeInTheDocument(); + }); + + it("renders grouped items with group labels", async () => { + const user = userEvent.setup(); + const items = [ + { + group: "Group 1", + key: "group-1", + items: [ + { label: "Group 1 Item 1", onClick: jest.fn() }, + { label: "Group 1 Item 2", onClick: jest.fn() }, + ], + }, + { + group: "Group 2", + key: "group-2", + items: [{ label: "Group 2 Item 1", onClick: jest.fn() }], + }, + ]; + + render(); + + await user.click(screen.getByTestId("dropdown-trigger")); + + expect(await screen.findByText("Group 1")).toBeInTheDocument(); + expect(screen.getByText("Group 2")).toBeInTheDocument(); + expect(screen.getByText("Group 1 Item 1")).toBeInTheDocument(); + expect(screen.getByText("Group 1 Item 2")).toBeInTheDocument(); + expect(screen.getByText("Group 2 Item 1")).toBeInTheDocument(); + }); + + it("hides group label when hideLabel is true", async () => { + const user = userEvent.setup(); + const items = [ + { + group: "Hidden Group", + key: "hidden-group", + hideLabel: true, + items: [{ label: "Hidden Group Item", onClick: jest.fn() }], + }, + ]; + + render(); + + await user.click(screen.getByTestId("dropdown-trigger")); + + expect(await screen.findByText("Hidden Group Item")).toBeInTheDocument(); + expect(screen.queryByText("Hidden Group")).not.toBeInTheDocument(); + }); + + it("filters items based on condition function", async () => { + const user = userEvent.setup(); + const items = [ + { label: "Visible Item", onClick: jest.fn(), condition: () => true }, + { label: "Hidden Item", onClick: jest.fn(), condition: () => false }, + ]; + + render(); + + await user.click(screen.getByTestId("dropdown-trigger")); + + expect(await screen.findByText("Visible Item")).toBeInTheDocument(); + expect(screen.queryByText("Hidden Item")).not.toBeInTheDocument(); + }); + + it("renders items with red theme styling", async () => { + const user = userEvent.setup(); + const items = [ + { label: "Delete", onClick: jest.fn(), theme: "red" as const }, + ]; + + render(); + + await user.click(screen.getByTestId("dropdown-trigger")); + + const deleteButton = await screen.findByTestId("dropdown-item-button"); + expect(deleteButton).toHaveTextContent("Delete"); + expect(deleteButton).toHaveClass("text-ink-red-3"); + }); + + it("renders items with icon as string", async () => { + const user = userEvent.setup(); + const items = [{ label: "Edit", onClick: jest.fn(), icon: "edit" }]; + + render(); + + await user.click(screen.getByTestId("dropdown-trigger")); + + expect(await screen.findByText("Edit")).toBeInTheDocument(); + // Icon should be rendered as FeatherIcon + const itemButton = screen.getByTestId("dropdown-item-button"); + expect(itemButton.querySelector("svg")).toBeInTheDocument(); + }); + + it("renders items with icon as React element", async () => { + const user = userEvent.setup(); + const CustomIcon = () => ( + CustomIcon placeholder + ); + const items = [ + { label: "CustomIcon", onClick: jest.fn(), icon: }, + ]; + + render(); + + await user.click(screen.getByTestId("dropdown-trigger")); + + expect(await screen.findByText("CustomIcon")).toBeInTheDocument(); + expect(screen.getByTestId("custom-icon")).toBeInTheDocument(); + }); + + it("renders switch item and toggles value", async () => { + const user = userEvent.setup(); + const onToggle = jest.fn(); + const items = [ + { + label: "Enable Feature", + onClick: onToggle, + switch: true, + switchValue: false, + }, + ]; + + render(); + + await user.click(screen.getByTestId("dropdown-trigger")); + + expect(await screen.findByText("Enable Feature")).toBeInTheDocument(); + + // Find and click the switch + const switchElement = screen.getByRole("switch"); + await user.click(switchElement); + + expect(onToggle).toHaveBeenCalledWith(true); + }); + + it("handles mixed items and groups", async () => { + const user = userEvent.setup(); + const items = [ + { label: "Standalone Item 1", onClick: jest.fn() }, + { + group: "Actions", + key: "actions-group", + items: [ + { label: "Action 1", onClick: jest.fn() }, + { label: "Action 2", onClick: jest.fn() }, + ], + }, + { label: "Standalone Item 2", onClick: jest.fn() }, + ]; + + render(); + + await user.click(screen.getByTestId("dropdown-trigger")); + + expect(await screen.findByText("Standalone Item 1")).toBeInTheDocument(); + expect(screen.getByText("Actions")).toBeInTheDocument(); + expect(screen.getByText("Action 1")).toBeInTheDocument(); + expect(screen.getByText("Action 2")).toBeInTheDocument(); + expect(screen.getByText("Standalone Item 2")).toBeInTheDocument(); + }); + + it("filters out null and undefined items", async () => { + const user = userEvent.setup(); + const items = [ + { label: "Valid Item", onClick: jest.fn() }, + null, + undefined, + { label: "Another Valid Item", onClick: jest.fn() }, + ]; + + // @ts-expect-error - testing null/undefined handling + render(); + + await user.click(screen.getByTestId("dropdown-trigger")); + + expect(await screen.findByText("Valid Item")).toBeInTheDocument(); + expect(screen.getByText("Another Valid Item")).toBeInTheDocument(); + + const menuItems = screen.getAllByTestId("dropdown-item-button"); + expect(menuItems).toHaveLength(2); + }); + + it("renders with different placements", () => { + const { rerender } = render( + + ); + expect(screen.getByTestId("dropdown-trigger")).toBeInTheDocument(); + + rerender( + + ); + expect(screen.getByTestId("dropdown-trigger")).toBeInTheDocument(); + + rerender( + + ); + expect(screen.getByTestId("dropdown-trigger")).toBeInTheDocument(); + }); + + it("renders custom component in dropdown item", async () => { + const user = userEvent.setup(); + const CustomComponent = ({ active }: { active: boolean }) => ( +
+ Custom Content +
+ ); + const items = [{ label: "Custom", component: CustomComponent }]; + + render(); + + await user.click(screen.getByTestId("dropdown-trigger")); + + expect(await screen.findByTestId("custom-component")).toBeInTheDocument(); + expect(screen.getByText("Custom Content")).toBeInTheDocument(); + }); + + it("renders empty dropdown when no options provided", async () => { + const user = userEvent.setup(); + + render(); + + const trigger = screen.getByTestId("dropdown-trigger"); + await user.click(trigger); + + // Dropdown should open but have no items + expect( + screen.queryByTestId("dropdown-item-button") + ).not.toBeInTheDocument(); + }); + + it("renders items in groups filtered by condition", async () => { + const user = userEvent.setup(); + const items = [ + { + group: "Filtered Group", + key: "filtered-group", + items: [ + { + label: "Visible in Group", + onClick: jest.fn(), + condition: () => true, + }, + { + label: "Hidden in Group", + onClick: jest.fn(), + condition: () => false, + }, + ], + }, + ]; + + render(); + + await user.click(screen.getByTestId("dropdown-trigger")); + + expect(await screen.findByText("Visible in Group")).toBeInTheDocument(); + expect(screen.queryByText("Hidden in Group")).not.toBeInTheDocument(); + }); +}); diff --git a/packages/frappe-ui-react/src/components/toast/tests/toast.tsx b/packages/frappe-ui-react/src/components/toast/tests/toast.tsx new file mode 100644 index 00000000..ccfd7898 --- /dev/null +++ b/packages/frappe-ui-react/src/components/toast/tests/toast.tsx @@ -0,0 +1,401 @@ +import "@testing-library/jest-dom"; +import { screen, renderHook, act, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import ToastProvider from "../toastProvider"; +import { useToasts } from "../useToast"; + +const renderToastHook = () => { + return renderHook(() => useToasts(), { + wrapper: ToastProvider, + }); +}; + +describe("Toast", () => { + describe("Basic Toast Types", () => { + it("renders success toast with message", async () => { + const { result } = renderToastHook(); + + act(() => { + result.current.success("Operation successful"); + }); + + expect( + await screen.findByText("Operation successful") + ).toBeInTheDocument(); + }); + + it("renders error toast with message", async () => { + const { result } = renderToastHook(); + + act(() => { + result.current.error("Operation failed"); + }); + + expect(await screen.findByText("Operation failed")).toBeInTheDocument(); + }); + + it("renders info toast with message", async () => { + const { result } = renderToastHook(); + + act(() => { + result.current.info("Here is some information"); + }); + + expect( + await screen.findByText("Here is some information") + ).toBeInTheDocument(); + }); + + it("renders warning toast with message", async () => { + const { result } = renderToastHook(); + + act(() => { + result.current.warning("This is a warning"); + }); + + expect(await screen.findByText("This is a warning")).toBeInTheDocument(); + }); + + it("renders multiple toasts simultaneously", async () => { + const { result } = renderToastHook(); + + act(() => { + result.current.success("Success message"); + result.current.error("Error message"); + result.current.info("Info message"); + result.current.warning("Warning message"); + }); + + expect(await screen.findByText("Success message")).toBeInTheDocument(); + expect(screen.getByText("Error message")).toBeInTheDocument(); + expect(screen.getByText("Info message")).toBeInTheDocument(); + expect(screen.getByText("Warning message")).toBeInTheDocument(); + }); + }); + + describe("Toast create method", () => { + it("creates toast using create method with type", async () => { + const { result } = renderToastHook(); + + act(() => { + result.current.create({ + message: "Custom toast message", + type: "success", + }); + }); + + expect( + await screen.findByText("Custom toast message") + ).toBeInTheDocument(); + }); + + it("creates toast with custom id", async () => { + const { result } = renderToastHook(); + + let toastId = ""; + act(() => { + toastId = result.current.create({ + message: "Toast with custom ID", + type: "info", + id: "custom-toast-id", + }); + }); + + expect( + await screen.findByText("Toast with custom ID") + ).toBeInTheDocument(); + expect(toastId).toBe("custom-toast-id"); + }); + + it("returns a toast id when creating a toast", async () => { + const { result } = renderToastHook(); + + let toastId = ""; + + act(() => { + toastId = result.current.success("Test toast"); + }); + + expect(toastId).toBeDefined(); + expect(typeof toastId).toBe("string"); + }); + }); + + describe("Toast removal", () => { + it("removes a specific toast by id", async () => { + const { result } = renderToastHook(); + + let toastId = ""; + + act(() => { + toastId = result.current.success("Toast to be removed"); + }); + + expect( + await screen.findByText("Toast to be removed") + ).toBeInTheDocument(); + + act(() => { + result.current.remove(toastId); + }); + + await waitFor(() => { + expect( + screen.queryByText("Toast to be removed") + ).not.toBeInTheDocument(); + }); + }); + + it("removes all toasts with removeAll", async () => { + const { result } = renderToastHook(); + + act(() => { + result.current.success("First toast"); + result.current.error("Second toast"); + result.current.info("Third toast"); + }); + + expect(await screen.findByText("First toast")).toBeInTheDocument(); + expect(screen.getByText("Second toast")).toBeInTheDocument(); + expect(screen.getByText("Third toast")).toBeInTheDocument(); + + act(() => { + result.current.removeAll(); + }); + + await waitFor(() => { + expect(screen.queryByText("First toast")).not.toBeInTheDocument(); + expect(screen.queryByText("Second toast")).not.toBeInTheDocument(); + expect(screen.queryByText("Third toast")).not.toBeInTheDocument(); + }); + }); + }); + + describe("Toast with action", () => { + it("renders toast with action button", async () => { + const { result } = renderToastHook(); + + const onClickMock = jest.fn(); + + act(() => { + result.current.create({ + message: "Toast with action", + type: "info", + action: { + label: "Undo", + onClick: onClickMock, + }, + }); + }); + + expect(await screen.findByText("Toast with action")).toBeInTheDocument(); + expect(screen.getByText("Undo")).toBeInTheDocument(); + }); + + it("calls action onClick when action button is clicked", async () => { + const user = userEvent.setup(); + const { result } = renderToastHook(); + + const onClickMock = jest.fn(); + + act(() => { + result.current.create({ + message: "Toast with clickable action", + type: "info", + action: { + label: "Click me", + onClick: onClickMock, + }, + }); + }); + + const actionButton = await screen.findByText("Click me"); + await user.click(actionButton); + + expect(onClickMock).toHaveBeenCalledTimes(1); + }); + }); + + describe("Toast closable option", () => { + it("renders close button when closable is true (default)", async () => { + const { result } = renderToastHook(); + + act(() => { + result.current.success("Closable toast"); + }); + + expect(await screen.findByText("Closable toast")).toBeInTheDocument(); + expect(screen.getByLabelText("Close")).toBeInTheDocument(); + }); + + it("does not render close button when closable is false", async () => { + const { result } = renderToastHook(); + + act(() => { + result.current.create({ + message: "Non-closable toast", + type: "info", + closable: false, + }); + }); + + expect(await screen.findByText("Non-closable toast")).toBeInTheDocument(); + expect(screen.queryByLabelText("Close")).not.toBeInTheDocument(); + }); + + it("closes toast when close button is clicked", async () => { + const user = userEvent.setup(); + const { result } = renderToastHook(); + + act(() => { + result.current.success("Toast to close"); + }); + + expect(await screen.findByText("Toast to close")).toBeInTheDocument(); + + const closeButton = screen.getByLabelText("Close"); + await user.click(closeButton); + + await waitFor(() => { + expect(screen.queryByText("Toast to close")).not.toBeInTheDocument(); + }); + }); + }); + + describe("Toast promise", () => { + it("shows loading state while promise is pending", async () => { + const { result } = renderToastHook(); + + const promise = new Promise((resolve) => { + setTimeout(() => resolve("Success!"), 100); + }); + + act(() => { + result.current.promise(promise, { + loading: "Loading...", + success: "Done!", + error: "Failed!", + }); + }); + + expect(await screen.findByText("Loading...")).toBeInTheDocument(); + }); + + it("shows success message when promise resolves", async () => { + const { result } = renderToastHook(); + + const promise = Promise.resolve("Success data"); + + act(() => { + result.current.promise(promise, { + loading: "Loading...", + success: "Operation completed!", + error: "Failed!", + }); + }); + + expect( + await screen.findByText("Operation completed!") + ).toBeInTheDocument(); + }); + + it("shows success message from function when promise resolves", async () => { + const { result } = renderToastHook(); + + const promise = Promise.resolve({ name: "John" }); + + await act(async () => { + await result.current.promise(promise, { + loading: "Loading...", + success: (data) => `Hello, ${data.name}!`, + error: "Failed!", + }); + }); + + expect(await screen.findByText("Hello, John!")).toBeInTheDocument(); + }); + + it("shows error message when promise rejects", async () => { + const { result } = renderToastHook(); + + const promise = Promise.reject(new Error("Something went wrong")); + + act(() => { + result.current + .promise(promise, { + loading: "Loading...", + success: "Done!", + error: "Operation failed!", + }) + .catch(() => {}); // Catch to avoid unhandled rejection in test + }); + + expect(await screen.findByText("Operation failed!")).toBeInTheDocument(); + }); + + it("shows error message from function when promise rejects", async () => { + const { result } = renderToastHook(); + + const promise = Promise.reject(new Error("Custom error message")); + + act(() => { + result.current + .promise(promise, { + loading: "Loading...", + success: "Done!", + error: (err) => `Error: ${err.message}`, + }) + .catch(() => {}); // Catch to avoid unhandled rejection in test + }); + + expect( + await screen.findByText("Error: Custom error message") + ).toBeInTheDocument(); + }); + }); + + describe("useToasts hook", () => { + it("throws error when used outside ToastProvider", () => { + const consoleError = jest + .spyOn(console, "error") + .mockImplementation(() => {}); + + expect(() => { + renderHook(() => useToasts()); + }).toThrow("useToasts must be used within a ToastsProvider"); + + consoleError.mockRestore(); + }); + }); + + describe("Toast HTML sanitization", () => { + it("renders allowed HTML tags in message", async () => { + const { result } = renderToastHook(); + + act(() => { + result.current.create({ + message: "This is important and urgent", + type: "info", + }); + }); + + expect(await screen.findByText("important")).toBe("STRONG"); + expect(screen.getByText("urgent")).toBe("EM"); + }); + + it("renders link tags in message", async () => { + const { result } = renderToastHook(); + + act(() => { + result.current.create({ + message: 'Click here for more info', + type: "info", + }); + }); + + const linkElement = await screen.findByText("here"); + expect(linkElement.tagName).toBe("A"); + expect(linkElement).toHaveAttribute("href", "https://example.com"); + }); + }); +}); diff --git a/packages/frappe-ui-react/src/components/toast/toast.stories.tsx b/packages/frappe-ui-react/src/components/toast/toast.stories.tsx index c0056e7b..6107ee5b 100644 --- a/packages/frappe-ui-react/src/components/toast/toast.stories.tsx +++ b/packages/frappe-ui-react/src/components/toast/toast.stories.tsx @@ -1,4 +1,4 @@ -import { Meta } from "@storybook/react-vite/*"; +import type { Meta } from "@storybook/react-vite/*"; import { useToasts } from "./useToast"; import ToastProvider from "./toastProvider"; @@ -7,7 +7,7 @@ import { Button } from "../button"; export default { title: "Components/Toast", parameters: { docs: { source: { type: "code" } }, layout: "centered" }, - tags: ["autodocs"], + tags: ["autodocs"], decorators: [ (Story) => ( diff --git a/packages/frappe-ui-react/src/components/toast/toast.tsx b/packages/frappe-ui-react/src/components/toast/toast.tsx index cdbfd117..f1c7c20f 100644 --- a/packages/frappe-ui-react/src/components/toast/toast.tsx +++ b/packages/frappe-ui-react/src/components/toast/toast.tsx @@ -1,21 +1,17 @@ import React, { useMemo } from "react"; -import * as Toast from "@radix-ui/react-toast"; +import { Toast } from "@base-ui/react/toast"; import { CircleCheck, AlertTriangle, Info, X } from "lucide-react"; import type { ToastProps } from "./types"; -const ToastComponent: React.FC = ({ - open, - onOpenChange, - message, - type = "success", - icon, - closable = true, - duration = 5000, - action, -}) => { +const ToastComponent: React.FC = ({ toast }) => { + const icon = toast.data?.icon; + const type = toast.type || "success"; + const closable = toast.data?.closable ?? true; + const iconComponent = useMemo(() => { if (icon) return icon; + switch (type) { case "success": return ( @@ -26,115 +22,81 @@ const ToastComponent: React.FC = ({ ); case "error": - return ; + return ( + + ); default: return null; } }, [icon, type]); - const handleAction = () => { - action?.onClick?.(); - }; - return ( -
- {iconComponent} -
- {message && ( - + +
+ {iconComponent} +
+ +
+
+
+ + {closable && ( + + + )}
-
-
- {action && ( - - {action.label} - - )} - {closable && ( - - - - )} -
+ diff --git a/packages/frappe-ui-react/src/components/toast/toastProvider.tsx b/packages/frappe-ui-react/src/components/toast/toastProvider.tsx index f8b7d16a..b5645dba 100644 --- a/packages/frappe-ui-react/src/components/toast/toastProvider.tsx +++ b/packages/frappe-ui-react/src/components/toast/toastProvider.tsx @@ -1,58 +1,72 @@ -import React, { ReactNode, useCallback, useMemo, useState } from "react"; -import { ToastProvider, ToastViewport } from "@radix-ui/react-toast"; +import React, { ReactNode, useCallback, useMemo } from "react"; +import { Toast, type ToastObject } from "@base-ui/react/toast"; import DOMPurify from "dompurify"; import LoadingIndicator from "../loadingIndicator"; -import { ToastProps, ToastOptions, ToastPromiseOptions } from "./types"; -import { ToastContext } from "./context"; +import type { + ToastDataInternal, + ToastOptions, + ToastPromiseOptions, +} from "./types"; +import { ToastAPI, ToastContext } from "./context"; import ToastComponent from "./toast"; interface ToastsProviderProps { children: ReactNode; } + +// Simple counter for generating unique toast IDs. let toastIdCounter = 0; -const ToastsProvider: React.FC = ({ children }) => { - const [toasts, setToasts] = useState([]); - const create = useCallback((options: ToastOptions): string => { - const id = `toast-${toastIdCounter++}`; - const durationInMs = - options.duration != null ? options.duration * 1000 : 5000; +const ToastsProvider: React.FC = ({ children }) => { + const toastManager = Toast.createToastManager(); - const sanitizedMessage = DOMPurify.sanitize(options.message, { - ALLOWED_TAGS: ["a", "em", "strong", "i", "b", "u"], - }); + return ( + + {children} + + ); +}; - const toastItem: ToastProps = { - id: options?.id || id, - open: true, - message: sanitizedMessage, - type: options.type || "info", - duration: durationInMs, - action: options.action, - icon: options.icon, - closable: options.closable ?? true, - onOpenChange: () => null, - }; +const ToastContextProvider: React.FC = ({ children }) => { + const toastManager = Toast.useToastManager(); - setToasts((prev) => [...prev, toastItem]); - return toastItem.id; - }, []); + const create = useCallback( + (options: ToastOptions): string => { + const id = `toast-${toastIdCounter++}`; + const durationInMs = + options.closable === false + ? 0 + : options.duration + ? options.duration * 1000 + : 5000; - const remove = useCallback((id: string) => { - setToasts((prev) => prev.filter((t) => t.id !== id)); - }, []); + const sanitizedMessage = DOMPurify.sanitize(options.message, { + ALLOWED_TAGS: ["a", "em", "strong", "i", "b", "u"], + }); - const removeAll = useCallback(() => { - setToasts([]); - }, []); + return toastManager.add({ + id: options?.id || id, + timeout: durationInMs, + description: , + type: options.type || "info", + actionProps: { + children: options.action?.label, + onClick: options.action?.onClick, + }, + data: { + icon: options.icon, + closable: options.closable, + }, + }); + }, + [toastManager] + ); - const updateToastInState = useCallback( - (id: string, updates: Partial>) => { - setToasts((prev) => - prev.map((t) => (t.id === id ? { ...t, ...updates, open: true } : t)) - ); + const remove = useCallback( + (id: string) => { + toastManager.close(id); }, - [] + [toastManager] ); const promise = useCallback( @@ -60,53 +74,49 @@ const ToastsProvider: React.FC = ({ children }) => { promiseToResolve: Promise, options: ToastPromiseOptions ): Promise => { - const loadingDurationInSeconds = options.duration ?? 0; - const toastId = create({ - message: options.loading, - type: "info", - icon: , - duration: loadingDurationInSeconds, - closable: false, - }); - - try { - const data = await promiseToResolve; - const successMessage = - typeof options.success === "function" - ? options.success(data) - : options.success; - const successToastDurationInSeconds = - options.successDuration ?? options.duration ?? 5; - - updateToastInState(toastId, { - message: successMessage, + return toastManager.promise(promiseToResolve, { + loading: { + description: options.loading, + type: "info", + timeout: 0, + data: { + icon: , + closable: false, + }, + }, + success: (data) => ({ + description: + typeof options.success === "function" + ? options.success(data) + : options.success, type: "success", - duration: successToastDurationInSeconds * 1000, - icon: undefined, - closable: true, - }); - return data; - } catch (error) { - const errorMessage = - typeof options.error === "function" - ? options.error(error as TError) - : options.error; - const errorToastDurationInSeconds = - options.errorDuration ?? options.duration ?? 5; - - updateToastInState(toastId, { - message: errorMessage, + timeout: (options.successDuration ?? options.duration ?? 5) * 1000, + data: { + icon: undefined, + }, + }), + error: (error) => ({ + description: + typeof options.error === "function" + ? options.error(error as TError) + : options.error, type: "error", - duration: errorToastDurationInSeconds * 1000, - icon: undefined, - closable: true, - }); - throw error; - } + timeout: (options.errorDuration ?? options.duration ?? 5) * 1000, + data: { + icon: undefined, + }, + }), + }); }, - [create, updateToastInState] + [toastManager] ); + const removeAll = useCallback(() => { + toastManager.toasts.forEach((toast: ToastObject) => + toastManager.close(toast.id) + ); + }, [toastManager]); + const success = useCallback( (message: string, options: Omit = {}) => create({ message, type: "success", ...options }), @@ -128,7 +138,7 @@ const ToastsProvider: React.FC = ({ children }) => { [create] ); - const api = useMemo( + const api: ToastAPI = useMemo( () => ({ create, remove, @@ -141,23 +151,18 @@ const ToastsProvider: React.FC = ({ children }) => { }), [create, remove, removeAll, promise, success, error, warning, info] ); + return ( - - - {children} - {toasts.map((t) => ( - { - if (!isOpen) { - remove(t.id); - } - }} - /> - ))} - - - + + {children} + + + {toastManager.toasts.map((toast: ToastObject) => ( + + ))} + + + ); }; diff --git a/packages/frappe-ui-react/src/components/toast/types.ts b/packages/frappe-ui-react/src/components/toast/types.ts index 06173d02..564eb81a 100644 --- a/packages/frappe-ui-react/src/components/toast/types.ts +++ b/packages/frappe-ui-react/src/components/toast/types.ts @@ -1,3 +1,4 @@ +import type { ToastObject } from "@base-ui/react"; import type { ReactNode } from "react"; export interface ToastActionProps { @@ -7,15 +8,7 @@ export interface ToastActionProps { } export interface ToastProps { - open: boolean; - onOpenChange: (open: boolean) => void; - message: string; - type?: ToastType; - icon?: ReactNode; - closable?: boolean; - duration?: number; - action?: ToastActionProps; - id: string; + toast: ToastObject; } export type ToastType = "success" | "warning" | "error" | "info"; @@ -26,8 +19,14 @@ export interface ToastActionProps { altText?: string; } -export interface ToastOptions - extends Omit, "open" | "message"> { +export interface ToastOptions { + onOpenChange?: (open: boolean) => void; + type?: ToastType; + icon?: ReactNode; + closable?: boolean; + duration?: number; + action?: ToastActionProps; + id?: string; message: string; } @@ -39,3 +38,8 @@ export interface ToastPromiseOptions { errorDuration?: number; duration?: number; } + +export interface ToastDataInternal { + icon?: ReactNode; + closable?: boolean; +} diff --git a/packages/frappe-ui-react/src/components/tooltip/tests/tooltip.tsx b/packages/frappe-ui-react/src/components/tooltip/tests/tooltip.tsx new file mode 100644 index 00000000..cd0ed544 --- /dev/null +++ b/packages/frappe-ui-react/src/components/tooltip/tests/tooltip.tsx @@ -0,0 +1,242 @@ +import React from "react"; +import { render, screen, waitFor } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import userEvent from "@testing-library/user-event"; + +import Tooltip from "../tooltip"; +import { Button } from "../../button"; + +describe("Tooltip", () => { + describe("Basic Rendering", () => { + it("renders children without tooltip initially", () => { + render( + + + + ); + + expect(screen.getByText("Hover me")).toBeInTheDocument(); + expect(screen.queryByText("Tooltip text")).not.toBeInTheDocument(); + }); + + it("shows tooltip on hover", async () => { + const user = userEvent.setup(); + + render( + + + + ); + + const trigger = screen.getByText("Hover me"); + await user.hover(trigger); + + expect(await screen.findByText("Tooltip text")).toBeInTheDocument(); + }); + + it("hides tooltip on mouse leave", async () => { + const user = userEvent.setup(); + + render( + + + + ); + + const trigger = screen.getByText("Hover me"); + await user.hover(trigger); + + expect(await screen.findByText("Tooltip text")).toBeInTheDocument(); + + await user.unhover(trigger); + + await waitFor(() => { + expect(screen.queryByText("Tooltip text")).not.toBeInTheDocument(); + }); + }); + }); + + describe("Text Prop", () => { + it("renders tooltip with text content", async () => { + const user = userEvent.setup(); + + render( + + + + ); + + await user.hover(screen.getByText("Trigger")); + + expect( + await screen.findByText("This is tooltip text") + ).toBeInTheDocument(); + }); + + it("renders empty tooltip when text is empty string", async () => { + const user = userEvent.setup(); + + render( + + + + ); + + await user.hover(screen.getByText("Trigger")); + + await waitFor(() => { + expect(screen.queryByTestId("tooltip-popup")).not.toBeInTheDocument(); + }); + }); + }); + + describe("Body Prop", () => { + it("renders custom body content", async () => { + const user = userEvent.setup(); + + render( + Custom content
} + hoverDelay={0} + > + + + ); + + await user.hover(screen.getByText("Trigger")); + + expect(await screen.findByTestId("custom-body")).toBeInTheDocument(); + expect(screen.getByText("Custom content")).toBeInTheDocument(); + }); + + it("body takes precedence over text", async () => { + const user = userEvent.setup(); + + render( + Body content} + hoverDelay={0} + > + + + ); + + await user.hover(screen.getByText("Trigger")); + + expect(await screen.findByText("Body content")).toBeInTheDocument(); + expect(screen.queryByText("Text content")).not.toBeInTheDocument(); + }); + + it("renders complex custom body content", async () => { + const user = userEvent.setup(); + + render( + + Title +

Description text

+ + } + hoverDelay={0} + > + +
+ ); + + await user.hover(screen.getByText("Trigger")); + + expect(await screen.findByText("Title")).toBeInTheDocument(); + expect(screen.getByText("Description text")).toBeInTheDocument(); + }); + }); + + describe("Different Trigger Elements", () => { + it("works with button trigger", async () => { + const user = userEvent.setup(); + + render( + + + + ); + + await user.hover(screen.getByText("Button Trigger")); + + expect(await screen.findByText("Button tooltip")).toBeInTheDocument(); + }); + + it("works with span trigger", async () => { + const user = userEvent.setup(); + + render( + + Span Trigger + + ); + + await user.hover(screen.getByText("Span Trigger")); + + expect(await screen.findByText("Span tooltip")).toBeInTheDocument(); + }); + + it("works with div trigger", async () => { + const user = userEvent.setup(); + + render( + +
Div Trigger
+
+ ); + + await user.hover(screen.getByText("Div Trigger")); + + expect(await screen.findByText("Div tooltip")).toBeInTheDocument(); + }); + }); + + describe("Edge Cases", () => { + it("handles no text and no body gracefully", async () => { + const user = userEvent.setup(); + + render( + + + + ); + + await user.hover(screen.getByText("Trigger")); + + // Should not crash, trigger should still be rendered + expect(screen.getByText("Trigger")).toBeInTheDocument(); + + waitFor(() => { + expect(screen.queryByTestId("tooltip-popup")).not.toBeInTheDocument(); + }); + }); + + it("renders multiple tooltips independently", async () => { + const user = userEvent.setup(); + + render( + <> + + + + + + + + ); + + await user.hover(screen.getByText("First")); + expect(await screen.findByText("First tooltip")).toBeInTheDocument(); + + await user.unhover(screen.getByText("First")); + await user.hover(screen.getByText("Second")); + + expect(await screen.findByText("Second tooltip")).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/frappe-ui-react/src/components/tooltip/tooltip.stories.tsx b/packages/frappe-ui-react/src/components/tooltip/tooltip.stories.tsx index 7881dd41..1410b9c4 100644 --- a/packages/frappe-ui-react/src/components/tooltip/tooltip.stories.tsx +++ b/packages/frappe-ui-react/src/components/tooltip/tooltip.stories.tsx @@ -49,7 +49,11 @@ type Story = StoryObj; export const WithText: Story = { render: (args) => { return ( - + ); diff --git a/packages/frappe-ui-react/src/components/tooltip/tooltip.tsx b/packages/frappe-ui-react/src/components/tooltip/tooltip.tsx index df93d77c..0eb9b47b 100644 --- a/packages/frappe-ui-react/src/components/tooltip/tooltip.tsx +++ b/packages/frappe-ui-react/src/components/tooltip/tooltip.tsx @@ -1,13 +1,7 @@ import React, { useMemo } from "react"; import { TooltipProps } from "./types"; -import { - TooltipProvider, - Root, - TooltipPortal, - TooltipContent, - TooltipTrigger, - TooltipArrow, -} from "@radix-ui/react-tooltip"; +import { Tooltip } from "@base-ui/react/tooltip"; +import clsx from "clsx"; const TooltipComponent: React.FC = ({ children, @@ -41,23 +35,32 @@ const TooltipComponent: React.FC = ({ } return ( - - - {children} - + + + + {tooltipContent && ( - - {tooltipContent} - - + + + {tooltipContent} + + + + + + + )} - - - + + + ); };