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}`}
+
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);
+ });
+ }}>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{
+ event.preventDefault();
+ event.stopPropagation();
+ }}
+ />
+
+
+ {matches.map((o) => (
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+
+
+
+ );
+}
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) => (
+
+
+
+ {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 }) => (
+
+
+ {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 && (
+
+ )}
+
+ {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