diff --git a/package-lock.json b/package-lock.json index bf9b0a5c..1150054f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", "@next/mdx": "15.1.6", + "@radix-ui/react-tooltip": "^1.2.8", "@vercel/analytics": "^1.6.1", "class-variance-authority": "^0.7.1", "date-fns": "^4.1.0", @@ -27,11 +28,13 @@ "remark-mdx-frontmatter": "^5.0.0" }, "devDependencies": { + "@ariakit/react": "^0.4.21", "@csstools/postcss-global-data": "^3.0.0", "@ianvs/prettier-plugin-sort-imports": "^4.6.2", "@lehoczky/postcss-fluid": "^1.0.3", "@next/env": "15.1.7", - "@radix-ui/react-dialog": "^1.1.5", + "@radix-ui/react-dialog": "1.1.4", + "@radix-ui/react-select": "2.1.4", "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-visually-hidden": "^1.1.2", "@shikijs/transformers": "^2.3.2", @@ -142,6 +145,47 @@ "openapi-types": ">=7" } }, + "node_modules/@ariakit/core": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/@ariakit/core/-/core-0.4.18.tgz", + "integrity": "sha512-9urEa+GbZTSyredq3B/3thQjTcSZSUC68XctwCkJNH/xNfKN5O+VThiem2rcJxpsGw8sRUQenhagZi0yB4foyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ariakit/react": { + "version": "0.4.21", + "resolved": "https://registry.npmjs.org/@ariakit/react/-/react-0.4.21.tgz", + "integrity": "sha512-UjP99Y7cWxA5seRECEE0RPZFImkLGFIWPflp65t0BVZwlMw4wp9OJZRHMrnkEkKl5KBE2NR/gbbzwHc6VNGzsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ariakit/react-core": "0.4.21" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ariakit" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@ariakit/react-core": { + "version": "0.4.21", + "resolved": "https://registry.npmjs.org/@ariakit/react-core/-/react-core-0.4.21.tgz", + "integrity": "sha512-rUI9uB/gT3mROFja/ka7/JukkdljIZR3eq3BGiQqX4Ni/KBMDvPK8FvVLnC0TGzWcqNY2bbfve8QllvHzuw4fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ariakit/core": "0.4.18", + "@floating-ui/dom": "^1.0.0", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -3817,6 +3861,44 @@ "dev": true, "license": "MIT" }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.4" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -5265,6 +5347,13 @@ "node": ">=12.4.0" } }, + "node_modules/@radix-ui/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@radix-ui/primitive": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", @@ -5272,6 +5361,73 @@ "dev": true, "license": "MIT" }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz", + "integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.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-arrow/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "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-arrow/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "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-collection": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz", @@ -5299,12 +5455,971 @@ } } }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", - "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", - "dev": true, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "dev": true, + "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.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "dev": true, + "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.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz", + "integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "^2.6.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-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "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-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "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-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "dev": true, + "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.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.3.tgz", + "integrity": "sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "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-dismissable-layer/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "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-dismissable-layer/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "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-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "dev": true, + "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.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz", + "integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "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-scope/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "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-focus-scope/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "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-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.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-popper": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz", + "integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "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/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "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-popper/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "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-portal": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz", + "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "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/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "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/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "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-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "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.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.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-roving-focus": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", + "integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "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-select": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.4.tgz", + "integrity": "sha512-pOkb2u8KgO47j/h7AylCj7dJsm69BXcjkrvTqMptFqsE2i0p8lHkfgneXKjAgPzBMivnoMyt8o4KiV4wYzDdyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.3", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.1", + "@radix-ui/react-portal": "1.1.3", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.1", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "^2.6.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-select/node_modules/@radix-ui/react-collection": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz", + "integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "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-select/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "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-select/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "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-select/node_modules/@radix-ui/react-visually-hidden": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz", + "integrity": "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.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-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "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-tabs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz", + "integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "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-tooltip/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-tooltip/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-tooltip/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-tooltip/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-tooltip/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-tooltip/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-tooltip/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-tooltip/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-tooltip/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-tooltip/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-tooltip/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" @@ -5315,11 +6430,10 @@ } } }, - "node_modules/@radix-ui/react-context": { + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", - "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", - "dev": true, + "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": "*", @@ -5331,49 +6445,33 @@ } } }, - "node_modules/@radix-ui/react-dialog": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", - "integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==", - "dev": true, + "node_modules/@radix-ui/react-tooltip/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/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.5", - "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.2", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-portal": "1.1.4", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" + "@radix-ui/react-use-effect-event": "0.0.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" + "react": "^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.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", - "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", - "dev": true, + "node_modules/@radix-ui/react-tooltip/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" @@ -5384,40 +6482,29 @@ } } }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", - "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", - "dev": true, + "node_modules/@radix-ui/react-tooltip/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", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-escape-keydown": "1.1.0" - }, "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" + "react": "^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": { + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-rect": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", - "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", - "dev": true, + "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" @@ -5428,41 +6515,36 @@ } } }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", - "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", - "dev": true, + "node_modules/@radix-ui/react-tooltip/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-compose-refs": "1.1.1", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.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" + "react": "^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": { + "node_modules/@radix-ui/react-tooltip/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/@radix-ui/react-use-callback-ref": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", - "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.0" - }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -5473,121 +6555,83 @@ } } }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", - "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-layout-effect": "1.1.0" + "@radix-ui/react-use-callback-ref": "1.1.0" }, "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" + "react": "^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.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", - "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", - "dev": 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-compose-refs": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.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" + "react": "^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.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", - "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", - "dev": true, + "node_modules/@radix-ui/react-use-effect-event/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", - "dependencies": { - "@radix-ui/react-slot": "1.1.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" + "react": "^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.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", - "integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==", + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-collection": "1.1.2", - "@radix-ui/react-compose-refs": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0" + "@radix-ui/react-use-callback-ref": "1.1.0" }, "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" + "react": "^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.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", - "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.1" - }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -5598,43 +6642,31 @@ } } }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz", - "integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==", + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", "dev": true, "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.1", - "@radix-ui/react-context": "1.1.1", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-roving-focus": "1.1.2", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, "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" + "react": "^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": { + "node_modules/@radix-ui/react-use-rect": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", - "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", "dev": true, "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.0" + }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -5645,14 +6677,14 @@ } } }, - "node_modules/@radix-ui/react-use-controllable-state": { + "node_modules/@radix-ui/react-use-size": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", "dev": true, "license": "MIT", "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" + "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", @@ -5664,30 +6696,33 @@ } } }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", - "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", - "dev": 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-use-callback-ref": "1.1.0" + "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + "@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-layout-effect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", - "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", - "dev": true, + "node_modules/@radix-ui/react-visually-hidden/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": "*", @@ -5699,14 +6734,13 @@ } } }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz", - "integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==", - "dev": true, + "node_modules/@radix-ui/react-visually-hidden/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-primitive": "2.0.2" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -5723,6 +6757,31 @@ } } }, + "node_modules/@radix-ui/react-visually-hidden/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/rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", + "dev": true, + "license": "MIT" + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -6517,7 +7576,7 @@ "version": "19.0.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz", "integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" @@ -18708,6 +19767,16 @@ } } }, + "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", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index e1629932..09b52cd6 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", "@next/mdx": "15.1.6", + "@radix-ui/react-tooltip": "^1.2.8", "@vercel/analytics": "^1.6.1", "class-variance-authority": "^0.7.1", "date-fns": "^4.1.0", @@ -37,11 +38,13 @@ "remark-mdx-frontmatter": "^5.0.0" }, "devDependencies": { + "@ariakit/react": "^0.4.21", "@csstools/postcss-global-data": "^3.0.0", "@ianvs/prettier-plugin-sort-imports": "^4.6.2", "@lehoczky/postcss-fluid": "^1.0.3", "@next/env": "15.1.7", - "@radix-ui/react-dialog": "^1.1.5", + "@radix-ui/react-dialog": "1.1.4", + "@radix-ui/react-select": "2.1.4", "@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-visually-hidden": "^1.1.2", "@shikijs/transformers": "^2.3.2", diff --git a/src/app/(api)/api/[...slug]/page.tsx b/src/app/(api)/api/[...slug]/page.tsx index 3f822075..66776885 100644 --- a/src/app/(api)/api/[...slug]/page.tsx +++ b/src/app/(api)/api/[...slug]/page.tsx @@ -62,6 +62,7 @@ export const generateStaticParams = async () => { // Generate swagger slugs const schemas = await parseSchemas(); const operations = toOperations(schemas); + const operationSlugs = operations.map((op) => ({ slug: toRouteSegments(op.slug) })); return mdxSlugs.concat(operationSlugs); @@ -139,7 +140,7 @@ const Page = async ({ params }: PageProps) => { Request - + Response diff --git a/src/app/_styles/variables.css b/src/app/_styles/variables.css index 1916d653..21687afe 100644 --- a/src/app/_styles/variables.css +++ b/src/app/_styles/variables.css @@ -287,6 +287,7 @@ --layer-navigation-mobile: 12; --layer-navigation: 10; --layer-overlay: 200; + --layer-select: 300; /* Specific measurements */ --navbar-banner-height: 0px; /* Will be set by LegacySiteBanner. Remove when banner is removed.*/ @@ -296,7 +297,6 @@ var(--navbar-banner-height) + var(--navbar-top-height) + var(--navbar-bottom-height) ); - @media (--phablet-up) { --navbar-top-height: 80px; --container-margin-home: var(--space-l); diff --git a/src/components/ApiMedia/ApiMedia.tsx b/src/components/ApiMedia/ApiMedia.tsx index f9afd8ef..49919ea7 100644 --- a/src/components/ApiMedia/ApiMedia.tsx +++ b/src/components/ApiMedia/ApiMedia.tsx @@ -6,6 +6,7 @@ import { cx } from 'class-variance-authority'; import { Tag } from '@/components'; import { ChevronIcon } from '@/icons/Chevron'; +import { textualSchemaRules } from '@/lib/operations/util'; import { RequestBodyObject, ResponseObject, SchemaObject } from '@/lib/swagger/types'; import styles from './ApiMedia.module.css'; @@ -150,121 +151,8 @@ const Property = ({ ); }; -/* - Based on: - https://spec.openapis.org/oas/v3.0.3.html#properties - https://datatracker.ietf.org/doc/html/draft-wright-json-schema-validation-00#section-5 -*/ const ValidationRules = ({ schema }: { schema: SchemaObject }) => { - const rules = Object.entries(schema).reduce((acc, [key, value]) => { - if (!value) { - return acc; - } - - switch (key) { - // Validation keywords for numeric instances - case 'multipleOf': - acc.push(`multiple of ${value}`); - break; - case 'maximum': - if (!schema.minimum) { - acc.push(`length ≤ ${value}${schema.exclusiveMaximum ? ' (exclusive)' : ''}`); - } - break; - case 'minimum': - if (!schema.maximum) { - acc.push(`length ≥ ${value}${schema.exclusiveMinimum ? ' (exclusive)' : ''}`); - } - if (schema.maximum) { - acc.push(`length between ${value} and ${schema.maximum}`); - } - break; - - // Validation keywords for strings - case 'maxLength': - if (!schema.minLength) { - acc.push(`length ≤ ${value}`); - } - break; - case 'minLength': - if (!schema.maxLength) { - acc.push(`length ≥ ${value}`); - } - if (schema.maxLength) { - acc.push(`length between ${value} and ${schema.maxLength}`); - } - break; - case 'pattern': - acc.push(`${value}`); - break; - - // Validation keywords for arrays - case 'additionalItems': - if (value === false) { - acc.push('No additional items allowed'); - } - break; - case 'maxItems': - if (!schema.minItems) { - acc.push(`items ≤ ${value}`); - } - break; - case 'minItems': - if (!schema.maxItems) { - acc.push(`items ≥ ${value}`); - } - if (schema.maxItems) { - const uniqueText = schema.uniqueItems ? ' (unique)' : ''; - acc.push(`items between ${value} and ${schema.maxItems}${uniqueText}`); - } - break; - case 'uniqueItems': - if (value === true && !schema.minItems && !schema.maxItems) { - acc.push('Items must be unique'); - } - break; - - // Validation keywords for objects - case 'maxProperties': - acc.push(`length ≤ ${value}`); - break; - case 'minProperties': - acc.push(`length ≥ ${value}`); - break; - case 'additionalProperties': - if (value === false) { - acc.push('no additional properties allowed'); - } - break; - - // Validation keywords for any instance type - case 'enum': - acc.push(`Allowed values: ${value.join(', ')}`); - break; - case 'format': - // TODO: Decide what to do with format - // acc.push(`${value}`); - break; - case 'default': - acc.push(`Defaults to ${value}`); - break; - - // TODO: What should we render here? - case 'allOf': - acc.push('Must match all schemas'); - break; - case 'anyOf': - acc.push('Must match any schema'); - break; - case 'oneOf': - acc.push('Must match exactly one schema'); - break; - case 'not': - acc.push('Must not match schema'); - break; - } - return acc; - }, []); + const rules = textualSchemaRules(schema); if (rules.length) { return rules.map((rule, index) => ( diff --git a/src/components/ApiRequest/ApiRequest.module.css b/src/components/ApiRequest/ApiRequest.module.css index 9daaba93..f7a095c6 100644 --- a/src/components/ApiRequest/ApiRequest.module.css +++ b/src/components/ApiRequest/ApiRequest.module.css @@ -1,10 +1,25 @@ .request { grid-column: 1 / -1; + margin-block-end: var(--column-block-padding); } -.url { - display: flex; +.sandbox { + display: inline-flex; + align-items: center; + gap: var(--space-xs); +} + +.urlCopy { + display: inline-flex; column-gap: var(--space-xs); - margin-block-end: var(--column-block-padding); + word-break: break-all; + border: 1px solid var(--color-border); + border-radius: var(--border-radius-m); + padding: var(--space-3xs) var(--space-2xs); +} + +.url { + text-align: start; + padding-top: var(--space-5xs); } diff --git a/src/components/ApiRequest/ApiRequest.tsx b/src/components/ApiRequest/ApiRequest.tsx index fb404797..6083fa4d 100644 --- a/src/components/ApiRequest/ApiRequest.tsx +++ b/src/components/ApiRequest/ApiRequest.tsx @@ -1,21 +1,37 @@ +import { cx } from 'class-variance-authority'; + import { Tag } from '@/components'; +import { getParametersByParam, operationUrl } from '@/lib/operations/util'; import { ApiOperation } from '@/lib/swagger/types'; import { ApiGrid, ApiGridColumn, ApiGridRow } from '../ApiGrid'; import { ApiMediaResponse } from '../ApiMedia'; +import { ApiSandboxDialog } from '../ApiSandbox'; +import { ClipboardCopy } from '../ClipboardCopy/ClipboardCopy'; import styles from './ApiRequest.module.css'; -export const ApiRequest = (operation: ApiOperation) => { - const getParametersByParam = (param: string) => operation.parameters?.filter((p) => p.in === param); - const pathsParameters = getParametersByParam('path'); - const queryParameters = getParametersByParam('query'); +export const ApiRequest = ({ + operation, + operations, +}: { + operation: ApiOperation; + operations: ApiOperation[]; +}) => { + const pathsParameters = getParametersByParam(operation, 'path'); + const queryParameters = getParametersByParam(operation, 'query'); + + const url = operationUrl(operation); return ( <>
-
- - {`${process.env.NEXT_PUBLIC_CLOUDSMITH_API_URL}/${operation.version}${operation.path}`} +
+ + + {url} + + +
diff --git a/src/components/ApiSandbox/ApiSandboxDialog.module.css b/src/components/ApiSandbox/ApiSandboxDialog.module.css new file mode 100644 index 00000000..5e6c5646 --- /dev/null +++ b/src/components/ApiSandbox/ApiSandboxDialog.module.css @@ -0,0 +1,62 @@ +.overlay { + display: grid; + position: fixed; + z-index: var(--layer-overlay); + inset: 0; + overflow-y: auto; + animation: overlayShow 500ms ease-in-out; + background-color: var(--color-overlay-dark); + place-items: start center; +} + +.content { + --padding-block: var(--space-m); + --padding-inline: var(--space-l); + + background-color: var(--base-color-white); + position: relative; + width: 100%; + max-width: 1274px; + overflow: hidden; + animation: contentShow 500ms ease-in-out; + border-radius: var(--border-radius-l); + box-shadow: 0 0 2px 1px var(--color-dialog-box-shadow); +} + +.main { + display: flex; + justify-content: space-between; + flex-wrap: wrap; +} + +.main > * { + flex: 50%; +} + +@media (--phablet-up) { + .content { + margin-block: var(--space-l); + } +} + +@keyframes overlayShow { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +@keyframes contentShow { + from { + transform: translate(0, -3%); + opacity: 0; + } + + to { + transform: translate(0, 0); + opacity: 1; + } +} diff --git a/src/components/ApiSandbox/ApiSandboxDialog.tsx b/src/components/ApiSandbox/ApiSandboxDialog.tsx new file mode 100644 index 00000000..6e4693b1 --- /dev/null +++ b/src/components/ApiSandbox/ApiSandboxDialog.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +import * as RadixDialog from '@radix-ui/react-dialog'; +import { VisuallyHidden } from '@radix-ui/react-visually-hidden'; + +import { Button } from '@/components/Button'; +import { ApiOperation } from '@/lib/swagger/types'; + +import styles from './ApiSandboxDialog.module.css'; +import { Sandbox } from './Sandbox'; + +type ApiSandboxDialogProps = { + operation: ApiOperation; + operations: ApiOperation[]; +}; + +export const ApiSandboxDialog = ({ operation, operations }: ApiSandboxDialogProps) => { + const [open, setOpen] = useState(false); + + const [currentOperation, setCurrentOperation] = useState(operation); + + useEffect(() => { + if (open) { + setCurrentOperation(operation); + } + }, [open, operation]); + + return ( + + + + + + + + + + Try API + API Sandbox + + + +
+ setCurrentOperation(o)} + /> +
+
+
+
+
+ ); +}; diff --git a/src/components/ApiSandbox/Sandbox.tsx b/src/components/ApiSandbox/Sandbox.tsx new file mode 100644 index 00000000..035f3235 --- /dev/null +++ b/src/components/ApiSandbox/Sandbox.tsx @@ -0,0 +1,118 @@ +'use client'; + +import { useEffect, useMemo, useState } from 'react'; + +import { getHeaderOptions, getParametersByParam } from '@/lib/operations/util'; +import { ApiOperation } from '@/lib/swagger/types'; + +import SandboxInput from './SandboxInput'; +import SandboxOutput from './SandboxOutput'; + +type SandboxProps = { + currentOperation: ApiOperation; + operations: ApiOperation[]; + onChangeOperation: (op: ApiOperation) => void; +}; + +export const Sandbox = ({ currentOperation, operations, onChangeOperation }: SandboxProps) => { + const pathsParameters = useMemo( + () => getParametersByParam(currentOperation, 'path') ?? [], + [currentOperation], + ); + const queryParameters = useMemo( + () => getParametersByParam(currentOperation, 'query') ?? [], + [currentOperation], + ); + const bodyParameters = currentOperation.requestBody; + const headers = useMemo(() => getHeaderOptions(currentOperation), [currentOperation]); + + const [pathParamState, setPathParamState] = useState>({}); + const [queryParamState, setQueryParamState] = useState>({}); + const [bodyParamState, setBodyParamState] = useState>>({}); + + const [headersState, setHeadersState] = useState<{ + current: 'apikey' | 'basic'; + apikey: string; + basic: string; + }>({ + current: headers[0], + apikey: '', + basic: '', + }); + + const paramState = useMemo( + () => ({ path: pathParamState, query: queryParamState, body: bodyParamState }), + [pathParamState, queryParamState, bodyParamState], + ); + + const updatePathParam = (name: string, value: string) => { + setPathParamState((v) => ({ ...v, [name]: value })); + }; + const updateQueryParam = (name: string, value: string) => { + setQueryParamState((v) => ({ ...v, [name]: value })); + }; + const updateBodyParam = (media: string, name: string, value: string) => { + setBodyParamState((v) => ({ ...v, [media]: { ...v[media], [name]: value } })); + }; + + useEffect(() => { + setPathParamState(Object.fromEntries(pathsParameters.map((p) => [p.name, '']))); + }, [pathsParameters]); + + useEffect(() => { + setQueryParamState( + Object.fromEntries(queryParameters.map((p) => [p.name, `${p.schema?.default ?? ''}`])), + ); + }, [queryParameters]); + + useEffect(() => { + setBodyParamState( + Object.fromEntries( + Object.entries(bodyParameters?.content ?? {}).map((entry) => [ + entry[0], + Object.fromEntries( + Object.entries(entry[1].schema?.properties ?? {}).map((e) => [e[0], e[1].default ?? '']), + ), + ]), + ), + ); + }, [bodyParameters]); + + useEffect(() => { + if (headers.length > 0 && !headers.includes(headersState.current)) { + setHeadersState((s) => ({ ...s, current: headers[0] })); + } + }, [headers, headersState]); + + return ( + <> + setHeadersState((s) => ({ ...s, current: h }))} + onChangeHeader={(header, value) => setHeadersState((s) => ({ ...s, [header]: value }))} + onChangeOperation={onChangeOperation} + onUpdateState={(type, name, value, media = '') => { + if (type === 'path') updatePathParam(name, value); + if (type === 'query') updateQueryParam(name, value); + if (type === 'body') updateBodyParam(media, name, value); + }} + /> + + + ); +}; diff --git a/src/components/ApiSandbox/SandboxInput/SandboxInput.module.css b/src/components/ApiSandbox/SandboxInput/SandboxInput.module.css new file mode 100644 index 00000000..65e90be5 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/SandboxInput.module.css @@ -0,0 +1,39 @@ +.root { + min-width: 400px; + padding: var(--space-m); + height: calc(100vh - var(--space-l) * 2); + overflow-y: auto; +} + +@media (--phablet-up) { + .root { + height: calc(100vh - var(--space-l) * 2); + } +} + +.urlCopy { + width: 100%; + display: inline-flex; + column-gap: var(--space-xs); + + word-break: break-all; + border: 1px solid var(--color-border); + border-radius: var(--border-radius-m); + padding: var(--space-3xs) var(--space-2xs); +} + +.url { + text-align: start; + padding-top: var(--space-5xs); + margin-right: auto; +} + +.params { + --column-inline-padding: var(--space-m); + --column-block-padding: var(--space-m); + + width: 100%; + + /* display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; */ +} diff --git a/src/components/ApiSandbox/SandboxInput/SandboxInput.tsx b/src/components/ApiSandbox/SandboxInput/SandboxInput.tsx new file mode 100644 index 00000000..c628ebcc --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/SandboxInput.tsx @@ -0,0 +1,99 @@ +import { cx } from 'class-variance-authority'; + +import { ClipboardCopy } from '@/components/ClipboardCopy/ClipboardCopy'; +import { Flex } from '@/components/Flex'; +import { Tag } from '@/components/Tag'; +import { operationUrl } from '@/lib/operations/util'; +import { ApiOperation, ParameterObject, RequestBodyObject } from '@/lib/swagger/types'; + +import AuthInput from './components/AuthInput'; +import OperationSelect from './components/OperationSelect'; +import PathParams from './components/PathParams'; +import QueryParams from './components/QueryParams'; +import RequestBody from './components/RequestBody'; +import styles from './SandboxInput.module.css'; + +type SandboxInputProps = { + operation: ApiOperation; + operations: ApiOperation[]; + parameters: { + path: ParameterObject[]; + query: ParameterObject[]; + body: RequestBodyObject | undefined; + }; + paramState: { + path: Record; + query: Record; + body: Record>; + }; + currentHeader: 'apikey' | 'basic'; + headers: ('apikey' | 'basic')[]; + headersState: Record; + onUpdateCurrentHeader: (h: 'apikey' | 'basic') => void; + onChangeHeader: (h: 'apikey' | 'basic', value: string) => void; + onChangeOperation: (o: ApiOperation) => void; + onUpdateState: (type: 'path' | 'query' | 'body', name: string, value: string, media?: string) => void; +}; + +export const SandboxInput = ({ + operation, + operations, + parameters, + paramState, + headers, + currentHeader, + headersState, + onChangeHeader, + onChangeOperation, + onUpdateState, + onUpdateCurrentHeader, +}: SandboxInputProps) => { + const { path, query, body } = parameters; + + const url = operationUrl(operation); + + return ( + + + + + + {url} + + + + + + {path.length > 0 ? ( + onUpdateState('path', name, value)} + /> + ) : null} + + {query.length > 0 ? ( + onUpdateState('query', name, value)} + /> + ) : null} + + {body ? ( + onUpdateState('body', name, value, meta)} + /> + ) : null} + + + ); +}; diff --git a/src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.module.css b/src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.module.css new file mode 100644 index 00000000..c8ae836e --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.module.css @@ -0,0 +1,83 @@ +.root { + position: relative; + width: 100%; +} + +.select { + border: 1px solid var(--color-border); + width: fit-content; + padding: var(--space-xs); + border-radius: var(--border-radius-m) 0 0 var(--border-radius-m); + flex-shrink: 0; +} + +.input { + padding: var(--space-xs) 65px var(--space-xs) var(--space-xs); + width: 100%; + border-top: 1px solid var(--color-border); + border-left: 0; + border-right: 1px solid var(--color-border); + border-bottom: 1px solid var(--color-border); + border-radius: 0 var(--border-radius-m) var(--border-radius-m) 0; +} + +.selectContainer { + background-color: var(--color-background-default); + border-radius: var(--border-radius-m); + box-shadow: 0 0 2px 1px var(--color-dialog-box-shadow); + z-index: var(--layer-select); + border: 1px solid var(--color-border); + animation: contentShow 200ms ease-in-out; +} + +.selectItem { + position: relative; + display: flex; + cursor: pointer; + scroll-margin-top: var(--space-4xs); + scroll-margin-bottom: var(--space-4xs); + align-items: center; + padding: var(--space-xs) var(--space-l) var(--space-xs) var(--space-l); + outline: 2px solid transparent; + outline-offset: 2px; +} + +.selectItem:hover { + background-color: var(--color-background-info); +} + +.selectItemIndicator { + position: absolute; + left: var(--space-3xs); + top: var(--space-s); + color: var(--color-accent-default); +} + +.iconsContainer { + position: absolute; + top: var(--space-s); + right: var(--space-s); + color: var(--color-text-tertiary); +} + +.button { + cursor: pointer; +} + +.button.selected { + color: var(--color-text-secondary); +} + +.tooltip { + background: var(--color-background-default); + padding: var(--space-2xs) var(--space-xs); + color: var(--color-text-primary); + border-radius: var(--border-radius-m); + box-shadow: 0 0 2px 1px var(--color-dialog-box-shadow); + z-index: var(--layer-select); + border: 1px solid var(--color-border); +} + +.notes { + width: 100%; +} diff --git a/src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.tsx b/src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.tsx new file mode 100644 index 00000000..c88a086f --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.tsx @@ -0,0 +1,135 @@ +import { useState } from 'react'; + +import * as RadixSelect from '@radix-ui/react-select'; +import * as RadixTooltip from '@radix-ui/react-tooltip'; +import { cx } from 'class-variance-authority'; + +import { Flex } from '@/components/Flex'; +import { Note } from '@/components/Note'; +import { Icon } from '@/icons'; + +import styles from './AuthInput.module.css'; + +type AuthInputProps = { + currentHeader: 'apikey' | 'basic'; + headers: ('apikey' | 'basic')[]; + headersState: string; + onChangeHeader: (h: 'apikey' | 'basic', value: string) => void; + onUpdateCurrentHeader: (h: 'apikey' | 'basic') => void; +}; + +const authLabel = (header: 'apikey' | 'basic') => (header === 'apikey' ? 'API Key' : 'Basic'); + +const AuthInput = ({ + currentHeader, + headers, + headersState, + onChangeHeader, + onUpdateCurrentHeader, +}: AuthInputProps) => { + const [hideAuth, setHideAuth] = useState(false); + const [showNotes, setShowNotes] = useState(false); + + if (headers.length === 0) return null; + + return ( + <> + + + + + {headers.length >= 2 && } + +
{authLabel(currentHeader)}
+
+
+
+ + + + {headers.map((h) => ( + + + + + {authLabel(h)} + + ))} + + +
+ + onChangeHeader(currentHeader, e.target.value)} + /> + + + {headersState && ( + + )} + + + + + + + + {hideAuth ? 'Show' : 'Hide'} + + + + + + + + + + + Getting your API Key + + + + +
+ + {showNotes && ( + + You can{' '} + + find your API Key + {' '} + within your User Settings or you can request (or reset) it via the Users Token API Endpoint.
+
+ Cloudsmith Entitlement Tokens cannot be used to authenticate to the Cloudsmith API. Entitlement + Tokens are used to authenticate for package downloads only. +
+ )} + + ); +}; + +export default AuthInput; diff --git a/src/components/ApiSandbox/SandboxInput/components/AuthInput/index.ts b/src/components/ApiSandbox/SandboxInput/components/AuthInput/index.ts new file mode 100644 index 00000000..3a7995f6 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/AuthInput/index.ts @@ -0,0 +1,3 @@ +import AuthInput from './AuthInput'; + +export default AuthInput; diff --git a/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.module.css b/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.module.css new file mode 100644 index 00000000..8bbee533 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.module.css @@ -0,0 +1,113 @@ +.trigger { + padding: var(--space-2xs); + cursor: pointer; + border-radius: var(--border-radius-m); +} + +.trigger h2 { + margin: 0; +} + +.trigger[aria-expanded='true'] { + outline: solid 1px var(--color-accent-default); +} + +.content { + min-width: 430px; + overflow: hidden; + background-color: var(--color-background-default); + border-radius: var(--border-radius-m); + box-shadow: 0 0 2px 1px var(--color-dialog-box-shadow); + z-index: var(--layer-select); + max-height: 500px; + border: 1px solid var(--color-border); + animation: contentShow 200ms ease-in-out; +} + +.comboboxWrapper { + position: relative; + display: flex; + align-items: center; +} + +.combobox { + border-radius: var(--border-radius-s) var(--border-radius-s) 0 0; + border: 0; + border-bottom: 1px solid var(--color-border); + outline: none; + width: 100%; + padding: var(--space-xs) var(--space-m) var(--space-xs) var(--space-l); + font-family: var(--font-family-body); + font-size: var(--text-body-s); +} + +.combobox::placeholder { + color: var(--color-text-secondary); +} + +.comboboxIcon { + pointer-events: none; + position: absolute; + left: var(--space-2xs); +} + +.listbox { + overflow-y: auto; + padding: var(--space-4xs) 0; + display: grid; + grid-template-columns: minmax(50px, min-content) minmax(150px, auto); +} + +.item { + position: relative; + grid-column: 1 / -1; + display: grid; + column-gap: var(--space-2xs); + grid-template-columns: subgrid; + cursor: pointer; + scroll-margin-top: var(--space-4xs); + scroll-margin-bottom: var(--space-4xs); + align-items: center; + padding: var(--space-xs) var(--space-l) var(--space-xs) var(--space-l); + outline: 2px solid transparent; + outline-offset: 2px; +} + +.item:hover { + background-color: var(--color-background-info); +} + +.radixItem { + grid-column: 1 / -1; + display: grid; + grid-template-columns: subgrid; +} + +.itemMethod { + grid-column: 1; + display: flex; + justify-content: end; +} + +.itemTitle { + grid-column: 2; +} + +.itemIndicator { + position: absolute; + left: var(--space-3xs); + top: var(--space-s); + color: var(--color-accent-default); +} + +@keyframes contentShow { + from { + transform: translate(0, -3%); + opacity: 0; + } + + to { + transform: translate(0, 0); + opacity: 1; + } +} diff --git a/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.tsx b/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.tsx new file mode 100644 index 00000000..c501f5ff --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.tsx @@ -0,0 +1,127 @@ +import { startTransition, useMemo, useState } from 'react'; + +import { Combobox, ComboboxItem, ComboboxList, ComboboxProvider } from '@ariakit/react'; +import * as RadixSelect from '@radix-ui/react-select'; + +import { Flex } from '@/components/Flex'; +import { Heading } from '@/components/Heading'; +import { Tag } from '@/components/Tag'; +import { Icon } from '@/icons'; +import { operationKey } from '@/lib/operations/util'; +import { ApiOperation } from '@/lib/swagger/types'; + +import styles from './OperationSelect.module.css'; + +type OperationSelectProps = { + value: ApiOperation; + options: ApiOperation[]; + onValueChange: (o: ApiOperation) => void; +}; + +export default function OperationSelect({ value, options, onValueChange }: OperationSelectProps) { + const [open, setOpen] = useState(false); + const [searchValue, setSearchValue] = useState(''); + + const matches = useMemo(() => { + if (!searchValue) return options; + const search = searchValue.toLowerCase(); + const matches = options.filter( + (o) => + o.method.toLowerCase().includes(search) || + o.title.toLowerCase().includes(search) || + o.description?.toLowerCase().includes(search), + ); + // Radix Select does not work if we don't render the selected item, so we + // make sure to include it in the list of matches. + const selectedLanguage = options.find((op) => operationKey(op) === operationKey(value)); + if (selectedLanguage && !matches.includes(selectedLanguage)) { + matches.push(selectedLanguage); + } + return matches; + }, [searchValue, value, options]); + + return ( + { + const operation = options.find((o) => v === operationKey(o)); + if (operation) onValueChange(operation); + }} + open={open} + onOpenChange={setOpen}> + { + startTransition(() => { + setSearchValue(value); + }); + }}> + + + + +
+ {value.title} +
+
+
+
+ + +
+
+ +
+ { + event.preventDefault(); + event.stopPropagation(); + }} + /> +
+ + {matches.map((o) => ( + + + +
+
+ +
+ + {o.title} +
+
+ + + + +
+
+ ))} +
+
+
+
+ ); +} diff --git a/src/components/ApiSandbox/SandboxInput/components/OperationSelect/index.ts b/src/components/ApiSandbox/SandboxInput/components/OperationSelect/index.ts new file mode 100644 index 00000000..d626c406 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/OperationSelect/index.ts @@ -0,0 +1,3 @@ +import OperationSelect from './OperationSelect'; + +export default OperationSelect; diff --git a/src/components/ApiSandbox/SandboxInput/components/ParamSet/ParamSet.module.css b/src/components/ApiSandbox/SandboxInput/components/ParamSet/ParamSet.module.css new file mode 100644 index 00000000..b0ec59f5 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/ParamSet/ParamSet.module.css @@ -0,0 +1,75 @@ +/* ParamSet */ +.root { + border: 1px solid var(--color-border); + border-radius: var(--border-radius-m); + width: 100%; +} + +.header { + align-content: center; + border-top: 0; + border-radius: var(--border-radius-m) var(--border-radius-m) 0 0; + background-color: var(--color-background-light); + color: var(--brand-color-grey-7); +} + +.heading { + padding-inline: var(--column-inline-padding); + padding-block: calc(var(--column-block-padding) / 2); +} + +/* Param */ + +.param { + width: 100%; +} + +.main { + border-top: 1px solid var(--color-border); + padding: var(--space-xs) var(--space-xs) 0 var(--space-m); +} + +.main:not(:has(+ .details)) { + padding: var(--space-xs) var(--space-xs) var(--space-xs) var(--space-m); +} + +.details { + padding: var(--space-3xs) var(--space-xs) var(--space-xs) var(--space-m); +} + +.description { + font-size: var(--text-body-xs); + color: var(--color-text-tertiary); +} + +.basics { + max-width: 40%; + font-size: var(--text-body-xs); + row-gap: 0; +} + +.name { + font-size: var(--text-body-m); + text-overflow: ellipsis; + overflow: hidden; +} + +.paramType { + color: var(--base-color-blue-500); + font-family: var(--font-family-mono); + letter-spacing: var(--letter-http-type); +} + +/* ParamToggle */ + +.toggle { + --svg-path-fill: var(--color-text-tertiary); + + cursor: pointer; + color: var(--color-text-tertiary); + font-size: var(--text-body-s); + + width: 100%; + border-top: 1px solid var(--color-border); + padding: var(--space-xs) var(--space-xs) var(--space-xs) var(--space-m); +} diff --git a/src/components/ApiSandbox/SandboxInput/components/ParamSet/ParamSet.tsx b/src/components/ApiSandbox/SandboxInput/components/ParamSet/ParamSet.tsx new file mode 100644 index 00000000..8716c857 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/ParamSet/ParamSet.tsx @@ -0,0 +1,120 @@ +import { ReactNode } from 'react'; + +import { cx } from 'class-variance-authority'; + +import { Flex } from '@/components/Flex'; +import { Paragraph } from '@/components/Paragraph'; +import { Tag } from '@/components/Tag'; +import { Icon } from '@/icons'; +import { textualSchemaRules } from '@/lib/operations/util'; +import { NonArraySchemaObjectType, SchemaObject } from '@/lib/swagger/types'; + +import ParamInput from './inputs'; +import styles from './ParamSet.module.css'; + +type ParamSetProps = { + heading: ReactNode; + children: ReactNode; +}; + +const ParamSet = ({ heading, children }: ParamSetProps) => { + return ( +
+
+
{heading}
+
+ + {children} +
+ ); +}; + +const getParamDescription = (baseDescription: string, rules: string[]) => { + let final = baseDescription; + + if (final.length > 0 && !final.trimEnd().endsWith('.')) { + final = final.trimEnd() + '.'; + } + + rules.forEach((r) => { + final += ' ' + (r.length > 0 && !r.trimEnd().endsWith('.') ? r.trimEnd() + '.' : r); + }); + + return final; +}; + +type ParamProps = { + name: string; + description?: string; + schema?: SchemaObject; + required?: boolean; + value?: string; + onValueChange: (v: string) => void; +}; + +export const Param = ({ + name, + description, + schema, + required = false, + value = '', + onValueChange, +}: ParamProps) => { + // TODO: extend + const type = schema?.type as NonArraySchemaObjectType; + const typeLabel = + schema == null + ? 'string' + : (schema.type === 'array' + ? `${schema.type} of ${schema.items?.type}s` + : schema.format || schema.type) + (schema.nullable ? ' | null' : ''); + + const descriptionText = getParamDescription(description || '', textualSchemaRules(schema ?? {})); + return ( + + + +
{name}
+
{typeLabel}
+ + {required ? 'required' : 'optional'} +
+ + onValueChange(v)} + /> +
+ + {descriptionText && ( + + {descriptionText} + + )} +
+ ); +}; + +type ParamToggleProps = { + paramTag: string; + show: boolean; + onChangeShow: (s: boolean) => void; +}; + +export const ParamToggle = ({ paramTag, show, onChangeShow }: ParamToggleProps) => ( + + + +); + +export default ParamSet; diff --git a/src/components/ApiSandbox/SandboxInput/components/ParamSet/index.ts b/src/components/ApiSandbox/SandboxInput/components/ParamSet/index.ts new file mode 100644 index 00000000..485d91cf --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/ParamSet/index.ts @@ -0,0 +1,5 @@ +import ParamSet, { Param } from './ParamSet'; + +export { Param }; + +export default ParamSet; diff --git a/src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/ParamInput.module.css b/src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/ParamInput.module.css new file mode 100644 index 00000000..4d1fc62f --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/ParamInput.module.css @@ -0,0 +1,62 @@ +.root { + position: relative; + flex-shrink: 0; + width: 50%; + border: 1px solid var(--color-border); + border-radius: var(--border-radius-s); + padding: var(--space-4xs) var(--space-2xs); +} + +.select { + cursor: pointer; +} + +.reset { + position: absolute; + top: var(--space-2xs); + right: var(--space-xs); + cursor: pointer; +} + +.selectItem span, +.selectValue { + text-transform: capitalize; +} + +.selectContainer { + background-color: var(--color-background-default); + border-radius: var(--border-radius-m); + box-shadow: 0 0 2px 1px var(--color-dialog-box-shadow); + z-index: var(--layer-select); + border: 1px solid var(--color-border); + animation: contentShow 200ms ease-in-out; +} + +.selectItem { + position: relative; + display: flex; + cursor: pointer; + scroll-margin-top: var(--space-4xs); + scroll-margin-bottom: var(--space-4xs); + align-items: center; + padding: var(--space-xs) var(--space-l) var(--space-xs) var(--space-l); + outline: 2px solid transparent; + outline-offset: 2px; +} + +.selectItem:hover { + background-color: var(--color-background-info); +} + +.selectItemIndicator { + position: absolute; + left: var(--space-3xs); + top: var(--space-s); + color: var(--color-accent-default); +} + +.icon { + --svg-path-fill: var(--color-text-tertiary); + + color: var(--color-text-tertiary); +} diff --git a/src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/ParamInput.tsx b/src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/ParamInput.tsx new file mode 100644 index 00000000..51e48ad2 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/ParamInput.tsx @@ -0,0 +1,136 @@ +import { useCallback, useEffect, useState } from 'react'; + +import * as RadixSelect from '@radix-ui/react-select'; + +import { Flex } from '@/components/Flex'; +import { Icon } from '@/icons'; +import { NonArraySchemaObjectType } from '@/lib/swagger/types'; +import { debounce } from '@/lib/util'; + +import styles from './ParamInput.module.css'; + +type ParamInput = { + name: string; + type: NonArraySchemaObjectType; + value: string; + possibleValues: (string | number)[]; + onChange: (v: string) => void; +}; + +const ParamInput = ({ name, type, value: _value, possibleValues, onChange: _onChange }: ParamInput) => { + const [value, setValue] = useState(_value); + useEffect(() => setValue(_value), [_value]); + + const onChange = useCallback(debounce(_onChange, 200), [_onChange]); + + if (possibleValues?.length > 0) { + return ( +
+ + + + + + + {value} + + + + + + + {possibleValues.map((b) => ( + + + + + {`${b}`} + + ))} + + + + {value !== '' && ( + + )} +
+ ); + } + + if (type === 'boolean') { + return ( +
+ + + + + + + {value} + + + + + + + {['true', 'false'].map((b) => ( + + + + + {b} + + ))} + + + + {value !== '' && ( + + )} +
+ ); + } + + if (type === 'integer') + return ( + { + setValue(e.target.value); + onChange(e.target.value); + }} + /> + ); + + return ( + { + setValue(e.target.value); + onChange(e.target.value); + }} + /> + ); +}; + +export default ParamInput; diff --git a/src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/index.ts b/src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/index.ts new file mode 100644 index 00000000..e38faedf --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/index.ts @@ -0,0 +1,3 @@ +import ParamInput from './ParamInput'; + +export default ParamInput; diff --git a/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.tsx b/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.tsx new file mode 100644 index 00000000..fc8628e7 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.tsx @@ -0,0 +1,26 @@ +import { ApiOperation } from '@/lib/swagger/types'; + +import ParamSet, { Param } from '../ParamSet'; + +type PathParamsProps = { + parameters: NonNullable; + state: Record; + onUpdateParam: (name: string, value: string) => void; +}; + +const PathParams = ({ parameters, state, onUpdateParam }: PathParamsProps) => ( + + {parameters.map((param) => ( + onUpdateParam(param.name, v)} + /> + ))} + +); + +export default PathParams; diff --git a/src/components/ApiSandbox/SandboxInput/components/PathParams/index.ts b/src/components/ApiSandbox/SandboxInput/components/PathParams/index.ts new file mode 100644 index 00000000..bae748da --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/PathParams/index.ts @@ -0,0 +1,3 @@ +import PathParams from './PathParams'; + +export default PathParams; diff --git a/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.tsx b/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.tsx new file mode 100644 index 00000000..a232cfe6 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.tsx @@ -0,0 +1,45 @@ +import { useEffect, useMemo, useState } from 'react'; + +import { ApiOperation } from '@/lib/swagger/types'; + +import ParamSet, { Param } from '../ParamSet'; +import { ParamToggle } from '../ParamSet/ParamSet'; + +type QueryParamsProps = { + parameters: NonNullable; + state: Record; + onUpdateParam: (name: string, value: string) => void; +}; + +const QueryParams = ({ parameters, state, onUpdateParam }: QueryParamsProps) => { + const [showAll, setShowAll] = useState(false); + useEffect(() => { + setShowAll(false); + }, [parameters]); + const optionalExists = useMemo( + () => parameters.some((p) => p.required == null || !p.required), + [parameters], + ); + const displayedParameters = useMemo(() => { + return parameters.filter((param) => showAll || param.required); + }, [parameters, showAll]); + + return ( + + {displayedParameters.map((param) => ( + onUpdateParam(param.name, v)} + /> + ))} + {optionalExists && } + + ); +}; + +export default QueryParams; diff --git a/src/components/ApiSandbox/SandboxInput/components/QueryParams/index.ts b/src/components/ApiSandbox/SandboxInput/components/QueryParams/index.ts new file mode 100644 index 00000000..e736aba1 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/QueryParams/index.ts @@ -0,0 +1,3 @@ +import QueryParams from './QueryParams'; + +export default QueryParams; diff --git a/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.tsx b/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.tsx new file mode 100644 index 00000000..513fe570 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.tsx @@ -0,0 +1,99 @@ +import { useEffect, useMemo, useState } from 'react'; + +import { Tag } from '@/components/Tag'; +import { ApiOperation, SchemaObject } from '@/lib/swagger/types'; + +import ParamSet from '../ParamSet'; +import { Param, ParamToggle } from '../ParamSet/ParamSet'; + +type RequestBodyProps = { + requestBody: NonNullable; + state: Record>; + onUpdateParam: (meta: string, name: string, value: string) => void; +}; + +export const RequestBody = ({ state, requestBody, onUpdateParam }: RequestBodyProps) => { + const multipleMedia = Object.keys(requestBody.content).length > 1; + + return ( + <> + {Object.entries(requestBody.content).map(([media, spec]) => ( + onUpdateParam(media, name, value)} + /> + ))} + + ); +}; + +type MediaParamsProps = { + required: boolean; + media: string; + schema: SchemaObject; + multipleMedia: boolean; + state?: Record; + onUpdateParam: (name: string, value: string) => void; +}; + +const MediaParams = ({ + required, + media, + schema, + multipleMedia, + state = {}, + onUpdateParam, +}: MediaParamsProps) => { + const parameterEntries = useMemo(() => Object.entries(schema.properties ?? {}), [schema]); + + const [showAll, setShowAll] = useState(false); + + useEffect(() => { + setShowAll(false); + }, [parameterEntries]); + + const optionalExists = useMemo( + () => parameterEntries.some((p) => !schema.required?.includes(p[0])), + [parameterEntries], + ); + const displayedParameters = useMemo(() => { + return parameterEntries.filter((param) => showAll || schema.required?.includes(param[0])); + }, [parameterEntries, showAll]); + + if (schema.type !== 'object') { + return null; + } + + return ( + + Body params {multipleMedia ? `(${media})` : ''}{' '} + {required ? 'required' : 'optional'} + + }> + {displayedParameters.map((p) => { + const [name, param] = p; + return ( + onUpdateParam(name, value)} + /> + ); + })} + {optionalExists && } + + ); +}; + +export default RequestBody; diff --git a/src/components/ApiSandbox/SandboxInput/components/RequestBody/index.ts b/src/components/ApiSandbox/SandboxInput/components/RequestBody/index.ts new file mode 100644 index 00000000..3a823258 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/RequestBody/index.ts @@ -0,0 +1,3 @@ +import RequestBody from './RequestBody'; + +export default RequestBody; diff --git a/src/components/ApiSandbox/SandboxInput/index.ts b/src/components/ApiSandbox/SandboxInput/index.ts new file mode 100644 index 00000000..b8b80cdb --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/index.ts @@ -0,0 +1,3 @@ +import { SandboxInput } from './SandboxInput'; + +export default SandboxInput; diff --git a/src/components/ApiSandbox/SandboxOutput/SandboxOutput.module.css b/src/components/ApiSandbox/SandboxOutput/SandboxOutput.module.css new file mode 100644 index 00000000..f09b9f36 --- /dev/null +++ b/src/components/ApiSandbox/SandboxOutput/SandboxOutput.module.css @@ -0,0 +1,47 @@ +.root { + background-color: var(--color-background-pre); + padding: var(--space-m); + min-width: 400px; +} + +.root > * { + flex-shrink: 0; + width: 100%; +} + +@media (--phablet-up) { + .root { + max-height: calc(100vh - var(--space-l) * 2); + } +} + +.header { + color: var(--brand-color-grey-3); + margin-bottom: var(--space-xs); +} + +.header > p { + margin-bottom: 0; +} + +.headerButton { + padding: var(--space-m) var(--space-xl); +} + +.requestHeader { + opacity: 0.75; +} + +.request, +.response { + --border-radius: var(--border-radius-m); + + flex-shrink: 0; + margin-bottom: 0; +} + +.request :global(pre), +.response :global(pre) { + max-height: calc((100vh - var(--space-l) * 2) * 0.38); + overflow-y: auto; +} diff --git a/src/components/ApiSandbox/SandboxOutput/SandboxOutput.tsx b/src/components/ApiSandbox/SandboxOutput/SandboxOutput.tsx new file mode 100644 index 00000000..b007ceeb --- /dev/null +++ b/src/components/ApiSandbox/SandboxOutput/SandboxOutput.tsx @@ -0,0 +1,80 @@ +import { useEffect } from 'react'; + +import { Button } from '@/components/Button'; +import { CodeBlockSync } from '@/components/CodeBlock/CodeBlockSync'; +import { Flex } from '@/components/Flex'; +import { Note } from '@/components/Note'; +import { Paragraph } from '@/components/Paragraph'; +import { Tag } from '@/components/Tag'; +import { useApi } from '@/lib/operations/hooks'; +import { curlCommand, operationKey } from '@/lib/operations/util'; +import { ApiOperation } from '@/lib/swagger/types'; + +import styles from './SandboxOutput.module.css'; + +type SandboxOutputProps = { + operation: ApiOperation; + paramState: { + path: Record; + query: Record; + body: Record>; + }; + header: 'apikey' | 'basic' | null; + headerValue: string | null; +}; + +export const SandboxOutput = ({ operation, paramState, header, headerValue }: SandboxOutputProps) => { + const command = curlCommand(operation, paramState, [header, headerValue]); + + const { response, isFetching, call, reset } = useApi(operation, paramState, header, headerValue); + + const key = operationKey(operation); + useEffect(() => { + reset(); + }, [key, reset]); + + const statusTag = + (response?.status ?? -1) > 0 ? ( + + ) : null; + const stringResponse = + (response?.status ?? -1) > 0 && response?.body ? JSON.stringify(response.body, null, 4) : null; + + return ( + + + cURL Request + + + + + Request} + className={styles.request}> + {command} + + + {stringResponse ? ( + + {stringResponse} + + ) : null} + + {response?.status === null ? ( + + {response?.body?.error} + + ) : null} + + ); +}; diff --git a/src/components/ApiSandbox/SandboxOutput/index.ts b/src/components/ApiSandbox/SandboxOutput/index.ts new file mode 100644 index 00000000..701d06a3 --- /dev/null +++ b/src/components/ApiSandbox/SandboxOutput/index.ts @@ -0,0 +1,3 @@ +import { SandboxOutput } from './SandboxOutput'; + +export default SandboxOutput; diff --git a/src/components/ApiSandbox/index.ts b/src/components/ApiSandbox/index.ts new file mode 100644 index 00000000..61c4635b --- /dev/null +++ b/src/components/ApiSandbox/index.ts @@ -0,0 +1 @@ +export * from './ApiSandboxDialog'; diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 5f097747..07e2e9c5 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -1,8 +1,14 @@ -import { Icon, type IconName } from '@/icons'; -import { ButtonColorScheme, ButtonSize, ButtonVariant, ButtonWidth } from '@/lib/types'; -import { cva, cx, type VariantProps } from 'class-variance-authority'; -import Link from 'next/link'; import React from 'react'; + +import type { IconName } from '@/icons'; +import type { VariantProps } from 'class-variance-authority'; + +import { cva, cx } from 'class-variance-authority'; +import Link from 'next/link'; + +import { Icon } from '@/icons'; +import { ButtonColorScheme, ButtonSize, ButtonVariant, ButtonWidth } from '@/lib/types'; + import styles from './Button.module.css'; const buttonVariants = cva(styles.root, { @@ -43,7 +49,7 @@ const buttonVariants = cva(styles.root, { interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { - href: string; + href?: string; withArrow?: boolean; isExternalLink?: boolean; className?: string; @@ -87,6 +93,14 @@ export function Button({ )} ); + + if (href == null) + return ( + + ); + const linkProps = rest as React.AnchorHTMLAttributes; return ( diff --git a/src/components/ClipboardCopy/ClipboardCopy.module.css b/src/components/ClipboardCopy/ClipboardCopy.module.css new file mode 100644 index 00000000..ead95c93 --- /dev/null +++ b/src/components/ClipboardCopy/ClipboardCopy.module.css @@ -0,0 +1,34 @@ +.root { + cursor: pointer; + padding: 0; +} + +.icon { + height: 32px; + width: 32px; + color: var(--base-color-grey-600); + display: flex; + align-items: center; + justify-content: center; + position: relative; +} + +.default .icon { + height: 24px; + width: 24px; +} + +.pre .icon { + height: 32px; + width: 32px; +} + +.default:hover, +.default:hover .icon { + color: var(--base-color-blue-500); +} + +.pre:hover, +.pre:hover .icon { + color: var(--color-text-on-color); +} diff --git a/src/components/ClipboardCopy/ClipboardCopy.tsx b/src/components/ClipboardCopy/ClipboardCopy.tsx new file mode 100644 index 00000000..325c2612 --- /dev/null +++ b/src/components/ClipboardCopy/ClipboardCopy.tsx @@ -0,0 +1,75 @@ +'use client'; + +import { ReactNode, useState } from 'react'; + +import { cva } from 'class-variance-authority'; + +import { Icon, IconName } from '@/icons'; + +import styles from './ClipboardCopy.module.css'; + +const clipboard = cva(styles.root, { + variants: { + default: { + true: styles.default, + false: styles.pre, + }, + }, +}); + +export function ClipboardCopy({ + className, + textToCopy, + iconVariant = 'default', + children, +}: { + className?: string; + textToCopy: string; + iconVariant?: 'pre' | 'default'; + children?: ReactNode; +}) { + const [copyState, setCopyState] = useState('waiting'); + + async function copyText() { + try { + await navigator.clipboard.writeText(textToCopy); + setCopyState('copied'); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { + setCopyState('error'); + } + + setTimeout(() => { + setCopyState('waiting'); + }, 3000); + } + + return ( + + ); +} + +const getIconByState: Record = { + copied: 'action/check', + error: 'action/error', + waiting: 'action/copy', +}; + +type CopyStatus = 'copied' | 'error' | 'waiting'; diff --git a/src/components/CodeBlock/ClipboardCopy.module.css b/src/components/CodeBlock/ClipboardCopy.module.css deleted file mode 100644 index b2dc21ea..00000000 --- a/src/components/CodeBlock/ClipboardCopy.module.css +++ /dev/null @@ -1,15 +0,0 @@ -.button { - height: 32px; - width: 32px; - color: var(--base-color-grey-600); - cursor: pointer; - padding: 0; - display: flex; - align-items: center; - justify-content: center; - position: relative; - - &:hover { - color: var(--color-text-on-color); - } -} diff --git a/src/components/CodeBlock/ClipboardCopy.tsx b/src/components/CodeBlock/ClipboardCopy.tsx deleted file mode 100644 index dc7ea416..00000000 --- a/src/components/CodeBlock/ClipboardCopy.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { Icon, IconName } from '@/icons'; - -import styles from './ClipboardCopy.module.css'; - -export function ClipboardCopy({ textToCopy }: { textToCopy: string }) { - const [copyState, setCopyState] = useState('waiting'); - - async function copyText() { - try { - await navigator.clipboard.writeText(textToCopy); - setCopyState('copied'); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (_) { - setCopyState('error'); - } - - setTimeout(() => { - setCopyState('waiting'); - }, 3000); - } - - return ( - - ); -} - -const getIconByState: Record = { - copied: 'action/check', - error: 'action/error', - waiting: 'action/copy', -}; - -type CopyStatus = 'copied' | 'error' | 'waiting'; diff --git a/src/components/CodeBlock/CodeBlock.module.css b/src/components/CodeBlock/CodeBlock.module.css index c0ff3e38..3fea4759 100644 --- a/src/components/CodeBlock/CodeBlock.module.css +++ b/src/components/CodeBlock/CodeBlock.module.css @@ -4,6 +4,7 @@ --line-bg-color: rgb(255 255 255 / 15%); --line-number-size: 40px; --line-padding: 20px; + --border-radius: var(--border-radius-2xl); margin-block-end: var(--space-s); line-height: var(--line-height-xs); @@ -23,6 +24,11 @@ padding-block: 15px; } +.darker .code :global(pre) { + /* Needed to overwrite Shikit theme */ + background-color: var(--base-color-grey-1000) !important; +} + .code :global(code) { display: grid; tab-size: 4; @@ -58,18 +64,29 @@ position: relative; user-select: none; color: var(--color-text-on-color); - border-radius: var(--border-radius-2xl) var(--border-radius-2xl) 0 0; + border-radius: var(--border-radius) var(--border-radius) 0 0; padding-block: 5px; padding-inline: 21px; background: var(--color-background-pre) linear-gradient(var(--line-bg-color), var(--line-bg-color) 100%) center bottom / 100% 1px no-repeat; } +.darker .lang { + background: var(--base-color-grey-1000) linear-gradient(var(--line-bg-color), var(--line-bg-color) 100%) + center bottom / 100% 1px no-repeat; +} + .langText { opacity: 0.75; } .hideHeader { - border-top-left-radius: var(--border-radius-2xl); - border-top-right-radius: var(--border-radius-2xl); + border-top-left-radius: var(--border-radius); + border-top-right-radius: var(--border-radius); +} + +.error, +.loading { + padding: var(--space-s) var(--space-m); + color: var(--color-text-on-color); } diff --git a/src/components/CodeBlock/CodeBlock.tsx b/src/components/CodeBlock/CodeBlock.tsx index 7b74f860..76762966 100644 --- a/src/components/CodeBlock/CodeBlock.tsx +++ b/src/components/CodeBlock/CodeBlock.tsx @@ -1,11 +1,12 @@ import { transformerNotationHighlight } from '@shikijs/transformers'; import { cva, cx } from 'class-variance-authority'; -import { getHighlighter, theme } from '@/lib/highlight'; - -import { ClipboardCopy } from './ClipboardCopy'; +import { getHighlighter } from '@/lib/highlight/server'; +import { theme } from '@/lib/highlight/theme'; +import { ClipboardCopy } from '../ClipboardCopy/ClipboardCopy'; import styles from './CodeBlock.module.css'; +import { Props } from './props'; const codeBlock = cva(styles.root, { variants: { @@ -15,12 +16,23 @@ const codeBlock = cva(styles.root, { hideHeader: { true: styles.hideHeader, }, + darkerBackground: { + true: styles.darker, + }, }, }); -export async function CodeBlock({ children, lang, header = true }: Props) { - const hideHeader = !lang || !header; +export async function CodeBlock({ + variant = 'default', + children, + lang, + header, + hideHeader: _hideHeader = false, + className, +}: Props) { + const hideHeader = (!lang && !header) || _hideHeader; const hideLineNumbers = lang === 'bash' || lang === 'text'; + const darkerBackground = variant === 'darker'; const html = (await getHighlighter()).codeToHtml(children, { lang, @@ -32,20 +44,16 @@ export async function CodeBlock({ children, lang, header = true }: Props) { }); return ( -
+
{!hideHeader && (
-
{lang}
- +
+ {header ?? lang} +
+
)}
); } - -interface Props { - children: string; - lang: string; - header?: boolean; -} diff --git a/src/components/CodeBlock/CodeBlockSync.tsx b/src/components/CodeBlock/CodeBlockSync.tsx new file mode 100644 index 00000000..40db360b --- /dev/null +++ b/src/components/CodeBlock/CodeBlockSync.tsx @@ -0,0 +1,68 @@ +'use client'; + +import { transformerNotationHighlight } from '@shikijs/transformers'; +import { cva, cx } from 'class-variance-authority'; + +import { useHighlighter } from '@/lib/highlight/client'; +import { theme } from '@/lib/highlight/theme'; + +import { ClipboardCopy } from '../ClipboardCopy/ClipboardCopy'; +import styles from './CodeBlock.module.css'; +import { Props } from './props'; + +const codeBlock = cva(styles.root, { + variants: { + hideLineNumbers: { + false: styles.withLineNumbers, + }, + hideHeader: { + true: styles.hideHeader, + }, + darkerBackground: { + true: styles.darker, + }, + }, +}); + +export function CodeBlockSync({ + variant = 'default', + children, + lang, + header, + hideHeader: _hideHeader = false, + className, +}: Props) { + const hideHeader = (!lang && !header) || _hideHeader; + const hideLineNumbers = lang === 'bash' || lang === 'text'; + const darkerBackground = variant === 'darker'; + + const { highlighter, isFetching, isError } = useHighlighter(); + + const html = highlighter?.codeToHtml(children, { + lang, + theme, + transformers: [ + // Add more transformers when needed from https://shiki.style/packages/transformers + transformerNotationHighlight({ matchAlgorithm: 'v3' }), + ], + }); + + return ( +
+ {!hideHeader && ( +
+
+ {header ?? lang} +
+ +
+ )} + + {isFetching &&
Loading code block
} + + {isError &&
Something went wrong while rendering code block
} + + {html &&
} +
+ ); +} diff --git a/src/components/CodeBlock/index.ts b/src/components/CodeBlock/index.ts index 5f5f442d..c3556c49 100644 --- a/src/components/CodeBlock/index.ts +++ b/src/components/CodeBlock/index.ts @@ -1 +1 @@ -export * from './CodeBlock'; \ No newline at end of file +export * from './CodeBlock'; diff --git a/src/components/CodeBlock/props.ts b/src/components/CodeBlock/props.ts new file mode 100644 index 00000000..057488d8 --- /dev/null +++ b/src/components/CodeBlock/props.ts @@ -0,0 +1,10 @@ +import { ReactNode } from 'react'; + +export interface Props { + children: string; + className?: string; + lang: string; + header?: ReactNode; + hideHeader?: boolean; + variant?: 'default' | 'darker'; +} diff --git a/src/components/Note/Note.tsx b/src/components/Note/Note.tsx index 18dc43d6..6a2e54b2 100644 --- a/src/components/Note/Note.tsx +++ b/src/components/Note/Note.tsx @@ -1,5 +1,7 @@ import { cva, VariantProps } from 'class-variance-authority'; + import { Icon } from '@/icons'; + import styles from './Note.module.css'; const defaultHeadings = { @@ -29,9 +31,9 @@ const note = cva(styles.root, { }, }); -export function Note({ variant, headline, children, noHeadline, ...rest }: NoteProps) { +export function Note({ variant, headline, children, noHeadline, className, ...rest }: NoteProps) { return ( -
+
{noHeadline ? ( ) : ( diff --git a/src/components/Tag/Tag.module.css b/src/components/Tag/Tag.module.css index 47e677e2..e44feefc 100644 --- a/src/components/Tag/Tag.module.css +++ b/src/components/Tag/Tag.module.css @@ -120,18 +120,15 @@ } } - - - /* legacy tags */ .legacy { border-radius: 12px; - font-size: .75em; + font-size: 0.75em; font-weight: 500; padding: 2px 10px 1px 10px; text-decoration: none; - transition: background-color .4s; + transition: background-color 0.4s; display: inline-flex; flex-shrink: 0; @@ -144,7 +141,7 @@ } .blue { - --method-bg-color: #1478FF; + --method-bg-color: #1478ff; --method-text-color: #fff; } @@ -153,6 +150,11 @@ --method-text-color: #fff; } +.light-red { + --method-bg-color: #ffebef; + --method-text-color: #b73b55; +} + .purple { --method-bg-color: #6f6fef; --method-text-color: #fff; @@ -169,12 +171,12 @@ } .dark-grey { - --method-bg-color: #9D9D9D; + --method-bg-color: #9d9d9d; --method-text-color: #fff; } .dark-green { - --method-bg-color: #0D4449; + --method-bg-color: #0d4449; --method-text-color: #fff; } diff --git a/src/components/Tag/Tag.tsx b/src/components/Tag/Tag.tsx index c5a7028f..f9907b4c 100644 --- a/src/components/Tag/Tag.tsx +++ b/src/components/Tag/Tag.tsx @@ -1,4 +1,4 @@ -import { VariantProps, cva } from 'class-variance-authority'; +import { cva, VariantProps } from 'class-variance-authority'; import { OpenAPIV3 } from 'openapi-types'; import styles from './Tag.module.css'; @@ -23,6 +23,7 @@ const tagVariants = cva(styles.root, { yellow: styles.yellow, lightyellow: styles.lightyellow, red: styles.red, + 'light-red': styles['light-red'], grey: styles.grey, blue: styles.blue, @@ -64,13 +65,13 @@ const statusCodes: { [key in Tag.HttpResponseStatusCodes]: Tag.VariantsProps['va 200: 'green', 201: 'green', 204: 'green', - 400: 'red', - 401: 'red', - 402: 'red', - 403: 'red', - 404: 'red', - 417: 'red', - 422: 'red', + 400: 'light-red', + 401: 'light-red', + 402: 'light-red', + 403: 'light-red', + 404: 'light-red', + 417: 'light-red', + 422: 'light-red', }; export const Tag = ({ theme, size, type, active, mobileDarkMode, className, ...props }: Tag.Props) => { diff --git a/src/icons/icon-registry.tsx b/src/icons/icon-registry.tsx index cfe4bdb8..1d27fad6 100644 --- a/src/icons/icon-registry.tsx +++ b/src/icons/icon-registry.tsx @@ -1,31 +1,20 @@ -import { createIconRegistry } from './util/create-icon-registry'; - -import { RegoIcon } from './svgs/Rego'; -import { MenuIcon } from './svgs/Menu'; -import { ChevronIcon } from './svgs/Chevron'; -import { ChevronSmallIcon } from './svgs/ChevronSmall'; -import { ArrowIcon } from './svgs/Arrow'; -import { SearchIcon } from './svgs/Search'; -import { ExternalIcon } from './svgs/External'; -import { EditIcon } from './svgs/Edit'; -import { GithubIcon } from './svgs/Github'; -import { InfoIcon } from './svgs/Info'; - -import { UtilityDocumentationIcon } from './svgs/utility/Documentation'; -import { UtilityGuideIcon } from './svgs/utility/Guide'; -import { UtilityApiIcon } from './svgs/utility/Api'; - +import { ActionApiIcon } from './svgs/action/Api'; +import { ActionCheckIcon } from './svgs/action/Check'; import { CloseIcon } from './svgs/action/Close'; +import { ActionCopyIcon } from './svgs/action/Copy'; import { ActionDocumentationIcon } from './svgs/action/Documentation'; +import { ActionErrorIcon } from './svgs/action/Error'; +import { ActionEyeIcon } from './svgs/action/Eye'; +import { ActionEyeSlashedIcon } from './svgs/action/EyeSlashed'; import { ActionGuideIcon } from './svgs/action/Guide'; -import { ActionApiIcon } from './svgs/action/Api'; import { ActionLinkIcon } from './svgs/action/Link'; import { ActionPlayIcon } from './svgs/action/Play'; -import { ActionCopyIcon } from './svgs/action/Copy'; -import { ActionCheckIcon } from './svgs/action/Check'; -import { ActionErrorIcon } from './svgs/action/Error'; +import { ArrowIcon } from './svgs/Arrow'; +import { ChevronIcon } from './svgs/Chevron'; +import { ChevronSmallIcon } from './svgs/ChevronSmall'; +import { EditIcon } from './svgs/Edit'; import { EnterIcon } from './svgs/Enter'; - +import { ExternalIcon } from './svgs/External'; import { FormatAlpineIcon } from './svgs/format/Alpine'; import { FormatCargoIcon } from './svgs/format/Cargo'; import { FormatChocolateyIcon } from './svgs/format/Chocolatey'; @@ -39,9 +28,9 @@ import { FormatDebianIcon } from './svgs/format/Debian'; import { FormatDockerIcon } from './svgs/format/Docker'; import { FormatGoIcon } from './svgs/format/Go'; import { FormatGradleIcon } from './svgs/format/Gradle'; -import { FormatHuggingFaceIcon } from './svgs/format/HuggingFace'; import { FormatHelmIcon } from './svgs/format/Helm'; import { FormatHexIcon } from './svgs/format/Hex'; +import { FormatHuggingFaceIcon } from './svgs/format/HuggingFace'; import { FormatLuarocksIcon } from './svgs/format/Luarocks'; import { FormatMavenIcon } from './svgs/format/Maven'; import { FormatNpmIcon } from './svgs/format/Npm'; @@ -57,14 +46,14 @@ import { FormatSwiftIcon } from './svgs/format/Swift'; import { FormatTerraformIcon } from './svgs/format/Terraform'; import { FormatUnityIcon } from './svgs/format/Unity'; import { FormatVagrantIcon } from './svgs/format/Vagrant'; - +import { GithubIcon } from './svgs/Github'; +import { HomepageAPIIcon } from './svgs/homepage/API'; import { HomepageDocumentationIcon } from './svgs/homepage/Documentation'; import { HomepageGuideIcon } from './svgs/homepage/Guide'; -import { HomepageAPIIcon } from './svgs/homepage/API'; - -import { IntegrationArgoCDIcon } from './svgs/integration/ArgoCD'; +import { InfoIcon } from './svgs/Info'; import { IntegrationAikidoIcon } from './svgs/integration/Aikido'; import { IntegrationAnsibleIcon } from './svgs/integration/Ansible'; +import { IntegrationArgoCDIcon } from './svgs/integration/ArgoCD'; import { IntegrationAWSCodeBuildIcon } from './svgs/integration/AWSCodeBuild'; import { IntegrationAzureDevOpsIcon } from './svgs/integration/AzureDevOps'; import { IntegrationBitbucketPipelinesIcon } from './svgs/integration/BitbucketPipelines'; @@ -94,8 +83,16 @@ import { IntegrationTeamCityIcon } from './svgs/integration/TeamCity'; import { IntegrationTerraformIcon } from './svgs/integration/Terraform'; import { IntegrationTheiaIDEIcon } from './svgs/integration/TheiaIDE'; import { IntegrationTravisCIIcon } from './svgs/integration/TravisCI'; -import { IntegrationZapierIcon } from './svgs/integration/Zapier'; import { IntegrationVSCodeIcon } from './svgs/integration/VSCode'; +import { IntegrationZapierIcon } from './svgs/integration/Zapier'; +import { MenuIcon } from './svgs/Menu'; +import { QuestionIcon } from './svgs/Question'; +import { RegoIcon } from './svgs/Rego'; +import { SearchIcon } from './svgs/Search'; +import { UtilityApiIcon } from './svgs/utility/Api'; +import { UtilityDocumentationIcon } from './svgs/utility/Documentation'; +import { UtilityGuideIcon } from './svgs/utility/Guide'; +import { createIconRegistry } from './util/create-icon-registry'; export const iconRegistry = createIconRegistry({ menu: MenuIcon, @@ -127,6 +124,7 @@ export const iconRegistry = createIconRegistry({ edit: EditIcon, github: GithubIcon, info: InfoIcon, + question: QuestionIcon, 'utility/documentation': UtilityDocumentationIcon, 'utility/guide': UtilityGuideIcon, 'utility/api': UtilityApiIcon, @@ -139,6 +137,8 @@ export const iconRegistry = createIconRegistry({ 'action/copy': ActionCopyIcon, 'action/check': ActionCheckIcon, 'action/error': ActionErrorIcon, + 'action/eye': ActionEyeIcon, + 'action/eye-slashed': ActionEyeSlashedIcon, 'format/alpine': FormatAlpineIcon, 'format/cargo': FormatCargoIcon, 'format/chocolatey': FormatChocolateyIcon, diff --git a/src/icons/svgs/Question.tsx b/src/icons/svgs/Question.tsx new file mode 100644 index 00000000..b271480d --- /dev/null +++ b/src/icons/svgs/Question.tsx @@ -0,0 +1,18 @@ +import { createIcon, SpecificIconProps } from '../util/create-icon'; + +export const QuestionIcon = createIcon( + 'question', + ({ width = 16, height = 16, ...props }) => ({ + ...props, + width, + height, + viewBox: '0 0 16 16', + fill: 'none', + children: ( + + ), + }), +); diff --git a/src/icons/svgs/action/Eye.tsx b/src/icons/svgs/action/Eye.tsx new file mode 100644 index 00000000..1ad63a8e --- /dev/null +++ b/src/icons/svgs/action/Eye.tsx @@ -0,0 +1,20 @@ +import { createIcon } from '../../util/create-icon'; + +export const ActionEyeIcon = createIcon('action/eye', (props) => ({ + ...props, + fill: 'none', + children: ( + <> + + + + ), +})); diff --git a/src/icons/svgs/action/EyeSlashed.tsx b/src/icons/svgs/action/EyeSlashed.tsx new file mode 100644 index 00000000..22b5ad3a --- /dev/null +++ b/src/icons/svgs/action/EyeSlashed.tsx @@ -0,0 +1,12 @@ +import { createIcon } from '../../util/create-icon'; + +export const ActionEyeSlashedIcon = createIcon('action/eye-slashed', (props) => ({ + ...props, + fill: 'none', + children: ( + + ), +})); diff --git a/src/icons/svgs/action/eyeslashed.svg b/src/icons/svgs/action/eyeslashed.svg new file mode 100644 index 00000000..ba9ad5ee --- /dev/null +++ b/src/icons/svgs/action/eyeslashed.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/lib/highlight/client.ts b/src/lib/highlight/client.ts new file mode 100644 index 00000000..40e797f5 --- /dev/null +++ b/src/lib/highlight/client.ts @@ -0,0 +1,28 @@ +import { useEffect, useState } from 'react'; + +import type { Highlighter } from 'shiki'; + +import { getHighlighter } from './server'; + +export const useHighlighter = () => { + const [highlighter, setHighlighter] = useState(null); + const [fetching, setFetching] = useState(false); + const [error, setError] = useState(false); + + useEffect(() => { + if (!highlighter && !fetching) { + setFetching(true); + getHighlighter() + .then((h) => { + setHighlighter(h); + setFetching(false); + }) + .catch(() => { + setError(true); + setFetching(false); + }); + } + }, [highlighter, fetching]); + + return { highlighter, isFetching: fetching, isError: error }; +}; diff --git a/src/lib/highlight.tsx b/src/lib/highlight/server.ts similarity index 73% rename from src/lib/highlight.tsx rename to src/lib/highlight/server.ts index 74eb1c7e..caa5e23a 100644 --- a/src/lib/highlight.tsx +++ b/src/lib/highlight/server.ts @@ -1,7 +1,8 @@ +import type { Highlighter } from 'shiki'; -import { createHighlighter, type Highlighter } from 'shiki'; +import { createHighlighter } from 'shiki'; -export const theme = 'github-dark-default'; +import { theme } from './theme'; let highlighter: Highlighter | null = null; @@ -10,7 +11,7 @@ export async function getHighlighter() { highlighter = await createHighlighter({ themes: [theme], langs: [ - () => import('./lang/rego.json'), + () => import('../lang/rego.json'), 'js', 'jsx', 'ts', @@ -29,9 +30,9 @@ export async function getHighlighter() { 'xml', 'scala', 'python', - 'scss', + 'scss', 'ruby', - 'csv' + 'csv', ], }); } diff --git a/src/lib/highlight/theme.ts b/src/lib/highlight/theme.ts new file mode 100644 index 00000000..928ea4c1 --- /dev/null +++ b/src/lib/highlight/theme.ts @@ -0,0 +1 @@ +export const theme = 'github-dark-default'; diff --git a/src/lib/operations/hooks.ts b/src/lib/operations/hooks.ts new file mode 100644 index 00000000..7403c19f --- /dev/null +++ b/src/lib/operations/hooks.ts @@ -0,0 +1,100 @@ +import { useCallback, useState } from 'react'; + +import { operationUrl } from '@/lib/operations/util'; +import { ApiOperation } from '@/lib/swagger/types'; + +import { callApi } from './server'; + +export const useApi = ( + op: ApiOperation, + parameters: { + path: Record; + query: Record; + body: Record>; + }, + header: 'apikey' | 'basic' | null, + headerValue: string | null, +) => { + const [response, setResponse] = useState< + | { + status: null; + body: { error: string }; + } + | { + status: number; + body: object; + } + | undefined + >(undefined); + const [isFetching, setIsFetching] = useState(false); + + const call = () => { + setIsFetching(true); + setResponse(undefined); + + const baseUrl = operationUrl(op); + + const pathReplacedUrl = Object.entries(parameters.path).reduce((url, current) => { + const [param, value] = current; + const template = `{${param}}`; + if (value !== '' && url.includes(template)) { + return url.replaceAll(`{${param}}`, value); + } + return url; + }, baseUrl); + + const cleanedUrl = pathReplacedUrl.replaceAll('\{', '').replaceAll('\}', ''); + + const finalUrl = Object.entries(parameters.query) + .filter((entry) => entry[1] !== '') + .reduce((url, current, index) => { + let currenUrl: string = url; + if (index === 0) { + currenUrl += '?'; + } else { + currenUrl += '&'; + } + currenUrl += `${current[0]}=${current[1]}`; + + return currenUrl; + }, cleanedUrl); + + const headers: HeadersInit = { + accept: 'application/json', + 'content-type': 'application/json', + }; + + if (header && headerValue) { + const headerKey = header === 'apikey' ? 'X-Api-Key' : 'authorization'; + const value = header === 'apikey' ? headerValue : `Basic ${btoa(headerValue)}`; + headers[headerKey] = value; + } + + const body: Record = {}; + if (parameters.body && parameters.body['application/json']) { + const bodyParams = Object.entries(parameters.body['application/json']).filter( + (entry) => entry[1] != '', + ); + if (bodyParams.length > 0) { + bodyParams.forEach((param) => { + body[param[0]] = param[1]; + }); + } + } + + callApi(finalUrl, op.method, headers, Object.keys(body).length > 0 ? JSON.stringify(body) : undefined) + .then((response) => { + setResponse(response); + }) + .catch((response) => { + setResponse(response); + }) + .finally(() => setIsFetching(false)); + }; + + const reset = useCallback(() => { + setResponse(undefined); + }, [setResponse]); + + return { response, isFetching, call, reset }; +}; diff --git a/src/lib/operations/server.ts b/src/lib/operations/server.ts new file mode 100644 index 00000000..2826e6bc --- /dev/null +++ b/src/lib/operations/server.ts @@ -0,0 +1,35 @@ +'use server'; + +export const callApi = async ( + url: string, + method: string, + headers: HeadersInit, + body?: BodyInit, +): Promise< + | { + status: null; + body: { error: string }; + } + | { + status: number; + body: object; + } +> => { + try { + const response = await fetch(url, { + method, + body, + headers, + }); + const responseBody = await response.json(); + return { + status: response.status, + body: responseBody, + }; + } catch { + return { + status: null, + body: { error: 'Something went wrong. Please try again later.' }, + }; + } +}; diff --git a/src/lib/operations/util.ts b/src/lib/operations/util.ts new file mode 100644 index 00000000..5a75e899 --- /dev/null +++ b/src/lib/operations/util.ts @@ -0,0 +1,213 @@ +import { ApiOperation, SchemaObject } from '../swagger/types'; + +export const operationUrl = (operation: ApiOperation) => + `${process.env.NEXT_PUBLIC_CLOUDSMITH_API_URL}/${operation.version}${operation.path}/`; + +/** + * Turns an operation slug into a fully qualified local path to use in links + */ +export const operationPath = (slug: string): string => { + return `/api/${slug}`; +}; + +export const getParametersByParam = (operation: ApiOperation, param: string) => + operation.parameters?.filter((p) => p.in === param); + +export const getHeaderOptions = (operation: ApiOperation) => + Array.from( + new Set( + (operation.security ?? []) + .flatMap((s) => Object.keys(s)) + .filter((s) => s === 'apikey' || s === 'basic'), + ), + ).toSorted(); + +export const operationKey = (op: ApiOperation) => `${op.method}-${op.path}`; + +export const curlCommand = ( + op: ApiOperation, + parameters: { + path: Record; + query: Record; + body: Record>; + }, + _header: ['apikey' | 'basic' | null, string | null], +) => { + let command = `curl --request ${op.method.toUpperCase()} \\\n`; + + const baseUrl = operationUrl(op); + + const pathReplacedUrl = Object.entries(parameters.path).reduce((url, current) => { + const [param, value] = current; + const template = `{${param}}`; + if (value !== '' && url.includes(template)) { + return url.replaceAll(`{${param}}`, value); + } + return url; + }, baseUrl); + + const cleanedUrl = pathReplacedUrl.replaceAll('\{', '').replaceAll('\}', ''); + + const finalUrl = Object.entries(parameters.query) + .filter((entry) => entry[1] !== '') + .reduce((url, current, index) => { + let currenUrl: string = url; + if (index === 0) { + currenUrl += '?'; + } else { + currenUrl += '&'; + } + currenUrl += `${current[0]}=${current[1]}`; + + return currenUrl; + }, cleanedUrl); + + command += ` --url '${finalUrl}' \\\n`; + + const [header, headerValue] = _header; + + if (header && headerValue) { + const headerStart = header === 'apikey' ? 'X-Api-Key: ' : 'authorization: Basic '; + const headerEnd = header === 'apikey' ? headerValue : btoa(headerValue); + command += ` --header '${headerStart}${headerEnd}' \\\n`; + } + + command += ` --header 'accept:application/json' \\\n`; + command += ` --header 'content-type: application/json' `; + + if (parameters.body && parameters.body['application/json']) { + const bodyParams = Object.entries(parameters.body['application/json']).filter((entry) => entry[1] != ''); + if (bodyParams.length > 0) { + command += `\\\n`; + command += ` --data '\n`; + command += `{\n`; + bodyParams.forEach((param, index) => { + command += ` "${param[0]}": "${param[1]}"`; + if (index < bodyParams.length - 1) { + command += ','; + } + command += '\n'; + }); + command += `}\n`; + command += `'`; + } + } + + return command; +}; + +/* + Based on: + https://spec.openapis.org/oas/v3.0.3.html#properties + https://datatracker.ietf.org/doc/html/draft-wright-json-schema-validation-00#section-5 +*/ +export const textualSchemaRules = (schema: SchemaObject) => + Object.entries(schema).reduce((acc, [key, value]) => { + if (!value) { + return acc; + } + + switch (key) { + // Validation keywords for numeric instances + case 'multipleOf': + acc.push(`multiple of ${value}`); + break; + case 'maximum': + if (!schema.minimum) { + acc.push(`length ≤ ${value}${schema.exclusiveMaximum ? ' (exclusive)' : ''}`); + } + break; + case 'minimum': + if (!schema.maximum) { + acc.push(`length ≥ ${value}${schema.exclusiveMinimum ? ' (exclusive)' : ''}`); + } + if (schema.maximum) { + acc.push(`length between ${value} and ${schema.maximum}`); + } + break; + + // Validation keywords for strings + case 'maxLength': + if (!schema.minLength) { + acc.push(`length ≤ ${value}`); + } + break; + case 'minLength': + if (!schema.maxLength) { + acc.push(`length ≥ ${value}`); + } + if (schema.maxLength) { + acc.push(`length between ${value} and ${schema.maxLength}`); + } + break; + case 'pattern': + acc.push(`${value}`); + break; + + // Validation keywords for arrays + case 'additionalItems': + if (value === false) { + acc.push('No additional items allowed'); + } + break; + case 'maxItems': + if (!schema.minItems) { + acc.push(`items ≤ ${value}`); + } + break; + case 'minItems': + if (!schema.maxItems) { + acc.push(`items ≥ ${value}`); + } + if (schema.maxItems) { + const uniqueText = schema.uniqueItems ? ' (unique)' : ''; + acc.push(`items between ${value} and ${schema.maxItems}${uniqueText}`); + } + break; + case 'uniqueItems': + if (value === true && !schema.minItems && !schema.maxItems) { + acc.push('Items must be unique'); + } + break; + + // Validation keywords for objects + case 'maxProperties': + acc.push(`length ≤ ${value}`); + break; + case 'minProperties': + acc.push(`length ≥ ${value}`); + break; + case 'additionalProperties': + if (value === false) { + acc.push('no additional properties allowed'); + } + break; + + // Validation keywords for any instance type + case 'enum': + acc.push(`Allowed values: ${value.join(', ')}`); + break; + case 'format': + // TODO: Decide what to do with format + // acc.push(`${value}`); + break; + case 'default': + acc.push(`Defaults to ${value}`); + break; + + // TODO: What should we render here? + case 'allOf': + acc.push('Must match all schemas'); + break; + case 'anyOf': + acc.push('Must match any schema'); + break; + case 'oneOf': + acc.push('Must match exactly one schema'); + break; + case 'not': + acc.push('Must not match schema'); + break; + } + return acc; + }, []); diff --git a/src/lib/search/server.ts b/src/lib/search/server.ts index c52d7c9c..c9f2274b 100644 --- a/src/lib/search/server.ts +++ b/src/lib/search/server.ts @@ -1,14 +1,15 @@ 'use server'; -import path from 'path'; import { readFile } from 'fs/promises'; +import path from 'path'; + import { FullOptions, Searcher } from 'fast-fuzzy'; -import { SearchInput, SearchResult } from './types'; -import { parseSchemas, toOperations } from '../swagger/parse'; -import { apiOperationPath } from '../swagger/util'; import { contentPath, loadMdxInfo } from '../markdown/util'; import { extractMdxMetadata } from '../metadata/util'; +import { operationPath } from '../operations/util'; +import { parseSchemas, toOperations } from '../swagger/parse'; +import { SearchInput, SearchResult } from './types'; let fuzzySearcher: Searcher>; @@ -47,7 +48,7 @@ export const performSearch = async ( items.push({ title: op.title, content: op.description || 'Default content', - path: apiOperationPath(op.slug), + path: operationPath(op.slug), section: 'api', method: op.method, }); diff --git a/src/lib/swagger/parse.ts b/src/lib/swagger/parse.ts index d3cca489..cfcd6c97 100644 --- a/src/lib/swagger/parse.ts +++ b/src/lib/swagger/parse.ts @@ -5,8 +5,9 @@ import SwaggerParser from '@apidevtools/swagger-parser'; import { OpenAPIV3 } from 'openapi-types'; import { MenuItem } from '../menu/types'; +import { operationPath } from '../operations/util'; import { ApiOperation, ParameterObject } from './types'; -import { apiOperationPath, createSlug, createTitle, isHttpMethod, parseMenuSegments } from './util'; +import { createSlug, createTitle, isHttpMethod, parseMenuSegments } from './util'; const SCHEMAS_DIR = 'src/content/schemas'; @@ -92,6 +93,7 @@ export const toOperations = (schemas: { schema: OpenAPIV3.Document; version: str }; for (const { schema, version } of schemas) { + const defaultSecurity = schema.security; for (const path in schema.paths) { const pathObject = schema.paths[path]; for (const field in pathObject) { @@ -144,6 +146,7 @@ export const toOperations = (schemas: { schema: OpenAPIV3.Document; version: str slug, title: createTitle(menuSegments), experimental: operation.tags?.includes('experimental') === true, + security: operation.security ?? defaultSecurity, }); } } @@ -173,11 +176,11 @@ export const toMenuItems = (operations: ApiOperation[]): MenuItem[] => { if (!existing) { existing = { title }; if (isLast) { - existing.path = apiOperationPath(operation.slug); + existing.path = operationPath(operation.slug); existing.method = operation.method; } else { if (!existing.path) { - existing.path = apiOperationPath(operation.slug); + existing.path = operationPath(operation.slug); } existing.children = []; } diff --git a/src/lib/swagger/types.ts b/src/lib/swagger/types.ts index c499e946..fbd019cd 100644 --- a/src/lib/swagger/types.ts +++ b/src/lib/swagger/types.ts @@ -100,7 +100,7 @@ interface ParameterBaseObject { [media: string]: MediaTypeObject; }; } -type NonArraySchemaObjectType = 'boolean' | 'object' | 'number' | 'string' | 'integer'; +export type NonArraySchemaObjectType = 'boolean' | 'object' | 'number' | 'string' | 'integer'; type ArraySchemaObjectType = 'array'; export type SchemaObject = ArraySchemaObject | NonArraySchemaObject; interface ArraySchemaObject extends BaseSchemaObject { diff --git a/src/lib/swagger/util.ts b/src/lib/swagger/util.ts index 2e7db9b9..5e1e2e2d 100644 --- a/src/lib/swagger/util.ts +++ b/src/lib/swagger/util.ts @@ -1,4 +1,5 @@ import { OpenAPIV3 } from 'openapi-types'; + import { replaceAll, titleCase } from '../util'; export const isHttpMethod = (method: string): boolean => @@ -42,10 +43,3 @@ export const createSlug = (menuSegments: string[]): string => { export const createTitle = (menuSegments: string[]): string => { return menuSegments.join(' '); }; - -/** - * Turns an operation slug into a fully qualified local path to use in links - */ -export const apiOperationPath = (slug: string): string => { - return `/api/${slug}`; -}; diff --git a/src/lib/url.ts b/src/lib/url.ts new file mode 100644 index 00000000..e69de29b