diff --git a/package-lock.json b/package-lock.json index bf9b0a5c..ee939ebf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,11 +27,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 +144,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 +3860,48 @@ "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -5265,6 +5350,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 +5364,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", @@ -5332,26 +5491,50 @@ } }, "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==", + "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.5", + "@radix-ui/react-dismissable-layer": "1.1.3", "@radix-ui/react-focus-guards": "1.1.1", - "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.1", "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-portal": "1.1.3", "@radix-ui/react-presence": "1.1.2", - "@radix-ui/react-primitive": "2.0.2", - "@radix-ui/react-slot": "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.2.4", - "react-remove-scroll": "^2.6.3" + "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": "*", @@ -5368,6 +5551,25 @@ } } }, + "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", @@ -5385,15 +5587,15 @@ } }, "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==", + "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.2", + "@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" }, @@ -5412,6 +5614,49 @@ } } }, + "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", @@ -5429,14 +5674,14 @@ } }, "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==", + "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.2", + "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { @@ -5454,6 +5699,49 @@ } } }, + "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", @@ -5473,14 +5761,90 @@ } } }, + "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.4", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", - "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", + "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.2", + "@radix-ui/react-primitive": "2.0.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { @@ -5498,6 +5862,49 @@ } } }, + "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", @@ -5579,6 +5986,144 @@ } } }, + "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", @@ -5699,6 +6244,60 @@ } } }, + "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", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.0", + "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" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "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-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-visually-hidden": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz", @@ -5723,6 +6322,13 @@ } } }, + "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", @@ -18708,6 +19314,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..71dc3aa4 100644 --- a/package.json +++ b/package.json @@ -37,12 +37,14 @@ "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-tabs": "^1.1.3", + "@radix-ui/react-select": "2.1.4", "@radix-ui/react-visually-hidden": "^1.1.2", "@shikijs/transformers": "^2.3.2", "@svgr/webpack": "^8.1.0", diff --git a/src/app/(api)/api/[...slug]/page.tsx b/src/app/(api)/api/[...slug]/page.tsx index 3f822075..82c7f8c5 100644 --- a/src/app/(api)/api/[...slug]/page.tsx +++ b/src/app/(api)/api/[...slug]/page.tsx @@ -139,7 +139,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/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..75c2a8af --- /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]); + + 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..9f066d08 --- /dev/null +++ b/src/components/ApiSandbox/Sandbox.tsx @@ -0,0 +1,59 @@ +'use client'; + +import { useEffect, useMemo, useState } from 'react'; + +import { 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 [pathParamState, setPathParamState] = useState>({}); + + const paramState = useMemo(() => ({ path: pathParamState }), [[pathParamState]]); + + const updatePathParam = (name: string, value: string) => { + setPathParamState((v) => ({ ...v, [name]: value })); + }; + + useEffect(() => { + setPathParamState(Object.fromEntries(pathsParameters.map((p) => [p.name, '']))); + }, [pathsParameters]); + + return ( + <> + { + if (type === 'param') updatePathParam(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..3706c7dd --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/SandboxInput.module.css @@ -0,0 +1,39 @@ +.root { + min-width: 400px; + padding: var(--space-m); + max-height: calc(100vh - var(--space-l) * 2); + overflow-y: auto; +} + +@media (--phablet-up) { + .root { + max-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..114dceb9 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/SandboxInput.tsx @@ -0,0 +1,66 @@ +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 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; + }; + onChangeOperation: (o: ApiOperation) => void; + onUpdateState: (type: 'param' | 'query' | 'body', name: string, value: string) => void; +}; + +export const SandboxInput = ({ + operation, + operations, + onChangeOperation, + parameters, + paramState, + onUpdateState, +}: SandboxInputProps) => { + const { path, query, body } = parameters; + + const url = operationUrl(operation); + + return ( + + + + + + {url} + + +
+ {path.length > 0 ? ( + onUpdateState('param', name, value)} + /> + ) : null} + + {query.length > 0 ? : null} + + {body ? : null} +
+
+ ); +}; 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..54f28621 --- /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 300ms 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..118d20a6 --- /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]); + + 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/PathParams/PathParams.module.css b/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.module.css new file mode 100644 index 00000000..c0adbbfc --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.module.css @@ -0,0 +1,42 @@ +.root { + margin-block-end: var(--column-block-padding); + 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 { + border-top: 1px solid var(--color-border); + padding: var(--space-xs) var(--space-xs) var(--space-xs) var(--space-m); +} + +.name { + max-width: 50%; + font-size: var(--text-body-s); +} + +.input { + width: 50%; + border: 1px solid var(--color-border); + border-radius: var(--border-radius-s); + padding: var(--space-4xs) var(--space-2xs); +} + +.paramType { + color: var(--base-color-blue-500); + font-family: var(--font-family-mono); + letter-spacing: var(--letter-http-type); +} 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..a6f4f222 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.tsx @@ -0,0 +1,64 @@ +import { useCallback, useEffect, useState } from 'react'; + +import { cx } from 'class-variance-authority'; + +import { Flex } from '@/components/Flex'; +import { Tag } from '@/components/Tag'; +import { ApiOperation } from '@/lib/swagger/types'; +import { debounce } from '@/lib/util'; + +import styles from './PathParams.module.css'; + +type PathParamsProps = { + parameters: NonNullable; + state: Record; + onUpdateParam: (name: string, value: string) => void; +}; + +const ParamInput = ({ + value: _value, + onChange: _onChange, +}: { + value: string; + onChange: (v: string) => void; +}) => { + const [value, setValue] = useState(_value); + useEffect(() => setValue(_value), [_value]); + + const onChange = useCallback(debounce(_onChange, 300), [_onChange]); + + return ( + { + setValue(e.target.value); + onChange(e.target.value); + }} + /> + ); +}; + +const PathParams = ({ parameters, state, onUpdateParam }: PathParamsProps) => ( +
+
+
Path params
+
+ + {parameters.map((param) => ( + + +
{param.name}
+
{param.schema?.type}
+
+ {param.required ? 'required' : 'optional'} +
+
+ + 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.module.css b/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.module.css new file mode 100644 index 00000000..20c8ea80 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.module.css @@ -0,0 +1,40 @@ +.grid { + margin-block-end: var(--column-block-padding); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-m); + width: 100%; +} + +.item { + position: relative; + text-align: left; + border-top: 1px solid var(--color-border); +} + +.subItem { + padding-inline: var(--column-inline-padding); + padding-block: var(--column-block-padding); + + .header & { + grid-column: 1 / -1; + padding-block: calc(var(--column-block-padding) / 2); + } +} + +.param { + padding: var(--space-xs) var(--space-m); +} + +.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); +} + +.subItemType { + color: var(--base-color-blue-500); + font-family: var(--font-family-mono); + letter-spacing: var(--letter-http-type); +} 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..4b8e1ca6 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.tsx @@ -0,0 +1,28 @@ +import { cx } from 'class-variance-authority'; + +import { Flex } from '@/components/Flex'; +import { Tag } from '@/components/Tag'; +import { ApiOperation } from '@/lib/swagger/types'; + +import styles from './QueryParams.module.css'; + +const QueryParams = ({ parameters }: { parameters: NonNullable }) => ( +
+
+
Query params
+
+ {parameters.map((param) => ( +
+ +
{param.name}
+
{param.schema?.type}
+
+ {param.required ? 'required' : 'optional'} +
+
+
+ ))} +
+); + +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.module.css b/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.module.css new file mode 100644 index 00000000..20c8ea80 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.module.css @@ -0,0 +1,40 @@ +.grid { + margin-block-end: var(--column-block-padding); + border: 1px solid var(--color-border); + border-radius: var(--border-radius-m); + width: 100%; +} + +.item { + position: relative; + text-align: left; + border-top: 1px solid var(--color-border); +} + +.subItem { + padding-inline: var(--column-inline-padding); + padding-block: var(--column-block-padding); + + .header & { + grid-column: 1 / -1; + padding-block: calc(var(--column-block-padding) / 2); + } +} + +.param { + padding: var(--space-xs) var(--space-m); +} + +.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); +} + +.subItemType { + color: var(--base-color-blue-500); + font-family: var(--font-family-mono); + letter-spacing: var(--letter-http-type); +} 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..32139331 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.tsx @@ -0,0 +1,55 @@ +import { cx } from 'class-variance-authority'; + +import { Flex } from '@/components/Flex'; +import { Tag } from '@/components/Tag'; +import { ApiOperation, SchemaObject } from '@/lib/swagger/types'; + +import styles from './RequestBody.module.css'; + +export const RequestBody = ({ requestBody }: { requestBody: NonNullable }) => ( +
+
+
+ Body params{' '} + {requestBody.required && ( + + {requestBody.required ? 'required' : 'optional'} + + )} +
+
+ + {requestBody.content && + Object.entries(requestBody.content) + .map((entry) => { + const content = entry[1]; + const properties = { ...content.schema?.properties }; + if (content.schema?.required?.length) { + for (const s of content.schema?.required) { + properties[s].required = [s]; + } + } + return properties; + }) + .filter((v) => !!v) + .flatMap((p) => Object.entries(p)) + .flatMap((p) => { + const [name, param] = p as unknown as [string, SchemaObject]; + return ( +
+ +
{name}
+
{param?.type}
+
+ + {param?.required ? 'required' : 'optional'} + +
+
+
+ ); + })} +
+); + +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..80cc4c17 --- /dev/null +++ b/src/components/ApiSandbox/SandboxOutput/SandboxOutput.module.css @@ -0,0 +1,43 @@ +.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); +} + +.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.35); + 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..46bad26a --- /dev/null +++ b/src/components/ApiSandbox/SandboxOutput/SandboxOutput.tsx @@ -0,0 +1,94 @@ +import { Button } from '@/components/Button'; +import { CodeBlockSync } from '@/components/CodeBlock/CodeBlockSync'; +import { Flex } from '@/components/Flex'; +import { Paragraph } from '@/components/Paragraph'; +import { Tag } from '@/components/Tag'; +import { curlCommand } from '@/lib/operations/util'; +import { ApiOperation } from '@/lib/swagger/types'; + +import styles from './SandboxOutput.module.css'; + +const response = [ + { + access_private_broadcasts: true, + clients: 0, + created_at: '2026-01-12T20:18:38.445Z', + created_by: 'string', + created_by_url: 'string', + default: true, + disable_url: 'string', + downloads: 0, + enable_url: 'string', + eula_accepted: { + identifier: 'string', + number: 0, + }, + eula_accepted_at: '2026-01-12T20:18:38.445Z', + eula_accepted_from: 'string', + eula_required: true, + has_limits: true, + identifier: 0, + is_active: true, + is_limited: true, + limit_bandwidth: 0, + limit_bandwidth_unit: 'Byte', + limit_date_range_from: '2026-01-12T20:18:38.445Z', + limit_date_range_to: '2026-01-12T20:18:38.445Z', + limit_num_clients: 0, + limit_num_downloads: 0, + limit_package_query: 'string', + limit_path_query: 'string', + metadata: {}, + name: 'string', + refresh_url: 'string', + reset_url: 'string', + scheduled_reset_at: '2026-01-12T20:18:38.445Z', + scheduled_reset_period: 'Never Reset', + self_url: 'string', + slug_perm: 'string', + token: 'string', + updated_at: '2026-01-12T20:18:38.445Z', + updated_by: 'string', + updated_by_url: 'string', + usage: 'string', + user: 'string', + user_url: 'string', + }, +]; + +type SandboxOutputProps = { + operation: ApiOperation; + paramState: { + path: Record; + }; +}; + +export const SandboxOutput = ({ operation, paramState }: SandboxOutputProps) => { + const command = curlCommand(operation, paramState); + + const stringResponse = JSON.stringify(response, null, 4); + + return ( + + + cURL Request + + + + + + {command} + + + 200} + className={styles.response}> + {stringResponse} + + + ); +}; 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..0a84df62 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,14 @@ 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..9c1cb12f --- /dev/null +++ b/src/components/CodeBlock/CodeBlockSync.tsx @@ -0,0 +1,66 @@ +'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/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/util.ts b/src/lib/operations/util.ts new file mode 100644 index 00000000..c52e6c55 --- /dev/null +++ b/src/lib/operations/util.ts @@ -0,0 +1,44 @@ +import { ApiOperation } 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 operationKey = (op: ApiOperation) => `${op.method}-${op.path}`; + +export const curlCommand = ( + op: ApiOperation, + parameters: { + path: Record; + }, +) => { + 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('\}', ''); + + command += ` --url '${cleanedUrl}' \\\n`; + + command += ` --header 'accept:application/json'`; + + return command; +}; 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..661ceba9 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'; @@ -173,11 +174,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/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