From 026d53418c887f40f3f0c5bbc44c3c90bd19e173 Mon Sep 17 00:00:00 2001 From: Fernando Florenzano Date: Thu, 15 Jan 2026 12:51:21 -0300 Subject: [PATCH 01/11] fix: refactor toOperations to read path level parameter definitions --- src/lib/swagger/parse.ts | 53 ++++++++++++++++++++++++++++++++-------- src/lib/swagger/types.ts | 2 +- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/lib/swagger/parse.ts b/src/lib/swagger/parse.ts index 3b248851..d3cca489 100644 --- a/src/lib/swagger/parse.ts +++ b/src/lib/swagger/parse.ts @@ -5,7 +5,7 @@ import SwaggerParser from '@apidevtools/swagger-parser'; import { OpenAPIV3 } from 'openapi-types'; import { MenuItem } from '../menu/types'; -import { ApiOperation } from './types'; +import { ApiOperation, ParameterObject } from './types'; import { apiOperationPath, createSlug, createTitle, isHttpMethod, parseMenuSegments } from './util'; const SCHEMAS_DIR = 'src/content/schemas'; @@ -75,15 +75,41 @@ export const parseSchemas = async (): Promise<{ schema: OpenAPIV3.Document; vers * Adds version information and checks for duplicate endpoints. */ export const toOperations = (schemas: { schema: OpenAPIV3.Document; version: string }[]): ApiOperation[] => { - const operations: ApiOperation[] = []; + const pathOperationsMap = new Map(); + const getPathOperations = (path: string) => { + const pathParams: ApiOperation[] = pathOperationsMap.get(path) ?? []; + if (pathParams.length === 0) pathOperationsMap.set(path, pathParams); + return pathParams; + }; + const pathMethodTracker = new Map(); // Track path+method combinations and their versions + const pathParamsMap = new Map(); + const getPathParams = (path: string) => { + const pathParams: ParameterObject[] = pathParamsMap.get(path) ?? []; + if (pathParams.length === 0) pathParamsMap.set(path, pathParams); + return pathParams; + }; + for (const { schema, version } of schemas) { for (const path in schema.paths) { const pathObject = schema.paths[path]; - for (const method in pathObject) { - if (isHttpMethod(method)) { - const pathMethodKey = `${method.toUpperCase()} ${path}`; + for (const field in pathObject) { + if (field === 'parameters') { + // Casted as ParameterObject because schema should be dereferenced, so no ReferenceObject. + const params = (pathObject[field] as ParameterObject[]) ?? []; + // Save common params for path. + const pathParams = getPathParams(path); + pathParams.push(...params); + + // Update already processed operations for this path. + const pathOperations = getPathOperations(path); + pathOperationsMap.set( + path, + pathOperations.map((op) => ({ ...op, parameters: [...(op.parameters ?? []), ...pathParams] })), + ); + } else if (isHttpMethod(field)) { + const pathMethodKey = `${field.toUpperCase()} ${path}`; if (pathMethodTracker.has(pathMethodKey)) { const existingVersion = pathMethodTracker.get(pathMethodKey); @@ -93,19 +119,26 @@ export const toOperations = (schemas: { schema: OpenAPIV3.Document; version: str `Please ensure each endpoint exists in only one API version.`, ); } - pathMethodTracker.set(pathMethodKey, version); - const operation = pathObject[method as keyof OpenAPIV3.PathItemObject] as OpenAPIV3.OperationObject; + const operation = pathObject[field as keyof OpenAPIV3.PathItemObject] as OpenAPIV3.OperationObject; const menuSegments = parseMenuSegments(operation.operationId); const slug = createSlug(menuSegments); const cleanPath = path.replace(/\/$/, ''); - operations.push({ + const pathOperations = getPathOperations(path); + + // Get common path params + const commonPathParams = getPathParams(path); + // Casted as ParameterObject because schema should be dereferenced, so no ReferenceObject. + const pathParams = [...((operation.parameters as ParameterObject[]) ?? []), ...commonPathParams]; + + pathOperations.push({ ...(operation as ApiOperation), - method: method as OpenAPIV3.HttpMethods, + method: field as OpenAPIV3.HttpMethods, path: cleanPath, + parameters: pathParams, version, menuSegments, slug, @@ -117,7 +150,7 @@ export const toOperations = (schemas: { schema: OpenAPIV3.Document; version: str } } - return operations; + return Array.from(pathOperationsMap.values()).flat(); }; /** diff --git a/src/lib/swagger/types.ts b/src/lib/swagger/types.ts index 6ea79b3a..c499e946 100644 --- a/src/lib/swagger/types.ts +++ b/src/lib/swagger/types.ts @@ -78,7 +78,7 @@ type OperationObject = { servers?: ServerObject[]; } & T; -interface ParameterObject extends ParameterBaseObject { +export interface ParameterObject extends ParameterBaseObject { name: string; in: string; } From df23580a502e32876e59e7353e663009658213f6 Mon Sep 17 00:00:00 2001 From: Fernando Florenzano Date: Thu, 15 Jan 2026 19:09:45 -0300 Subject: [PATCH 02/11] chore: add non link button --- src/components/Button/Button.tsx | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) 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 ( From d20ff8201c27c51d351488a4b0875827533923c7 Mon Sep 17 00:00:00 2001 From: Fernando Florenzano Date: Thu, 15 Jan 2026 19:10:54 -0300 Subject: [PATCH 03/11] chore: extend clipboard copy component --- .../ClipboardCopy.module.css | 21 ++++-- .../ClipboardCopy/ClipboardCopy.tsx | 75 +++++++++++++++++++ src/components/CodeBlock/ClipboardCopy.tsx | 43 ----------- src/components/CodeBlock/CodeBlock.tsx | 5 +- 4 files changed, 92 insertions(+), 52 deletions(-) rename src/components/{CodeBlock => ClipboardCopy}/ClipboardCopy.module.css (53%) create mode 100644 src/components/ClipboardCopy/ClipboardCopy.tsx delete mode 100644 src/components/CodeBlock/ClipboardCopy.tsx diff --git a/src/components/CodeBlock/ClipboardCopy.module.css b/src/components/ClipboardCopy/ClipboardCopy.module.css similarity index 53% rename from src/components/CodeBlock/ClipboardCopy.module.css rename to src/components/ClipboardCopy/ClipboardCopy.module.css index b2dc21ea..d34d9df9 100644 --- a/src/components/CodeBlock/ClipboardCopy.module.css +++ b/src/components/ClipboardCopy/ClipboardCopy.module.css @@ -1,15 +1,24 @@ -.button { +.root { + cursor: pointer; + padding: 0; +} + +.icon { height: 32px; width: 32px; color: var(--base-color-grey-600); - cursor: pointer; - padding: 0; display: flex; align-items: center; justify-content: center; position: relative; +} + +.default:hover, +.default:hover .icon { + color: var(--base-color-blue-500); +} - &:hover { - color: var(--color-text-on-color); - } +.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.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.tsx b/src/components/CodeBlock/CodeBlock.tsx index 7b74f860..50b63a17 100644 --- a/src/components/CodeBlock/CodeBlock.tsx +++ b/src/components/CodeBlock/CodeBlock.tsx @@ -3,8 +3,7 @@ import { cva, cx } from 'class-variance-authority'; import { getHighlighter, theme } from '@/lib/highlight'; -import { ClipboardCopy } from './ClipboardCopy'; - +import { ClipboardCopy } from '../ClipboardCopy/ClipboardCopy'; import styles from './CodeBlock.module.css'; const codeBlock = cva(styles.root, { @@ -36,7 +35,7 @@ export async function CodeBlock({ children, lang, header = true }: Props) { {!hideHeader && (
{lang}
- +
)}
From 2d93a0e89c5a457378e6041f944cc78b8c6426d7 Mon Sep 17 00:00:00 2001 From: Fernando Florenzano Date: Thu, 15 Jan 2026 19:13:00 -0300 Subject: [PATCH 04/11] add: api operation clipboard copy and sandbox trigger --- src/app/(api)/api/[...slug]/page.tsx | 21 ++++++++------- .../ApiRequest/ApiRequest.module.css | 22 +++++++++++++-- src/components/ApiRequest/ApiRequest.tsx | 27 +++++++++++++++---- 3 files changed, 53 insertions(+), 17 deletions(-) diff --git a/src/app/(api)/api/[...slug]/page.tsx b/src/app/(api)/api/[...slug]/page.tsx index f1867fb5..e152f1f9 100644 --- a/src/app/(api)/api/[...slug]/page.tsx +++ b/src/app/(api)/api/[...slug]/page.tsx @@ -1,15 +1,16 @@ -import { ApiRequest, ApiResponses, TimeAgo, Heading, Paragraph, Tag, Note } from '@/components'; -import { loadMdxInfo } from '@/lib/markdown/util'; -import { parseSchemas, toOperations } from '@/lib/swagger/parse'; -import { toRouteSegments, toSlug } from '@/lib/util'; -import { notFound } from 'next/navigation'; import type { Metadata } from 'next'; -import { withMdxMetadata, withDefaultMetadata, getLastUpdated } from '@/lib/metadata/util'; -import { getMenuItem, getActiveAncestors } from '@/lib/menu/util'; + +import { cx } from 'class-variance-authority'; +import { notFound } from 'next/navigation'; + +import { ApiRequest, ApiResponses, Heading, Link, Note, Paragraph, Tag, TimeAgo } from '@/components'; import WithQuicknav from '@/components/WithQuickNav'; import { Icon } from '@/icons'; -import { Link } from '@/components'; -import { cx } from 'class-variance-authority'; +import { loadMdxInfo } from '@/lib/markdown/util'; +import { getActiveAncestors, getMenuItem } from '@/lib/menu/util'; +import { getLastUpdated, withDefaultMetadata, withMdxMetadata } from '@/lib/metadata/util'; +import { parseSchemas, toOperations } from '@/lib/swagger/parse'; +import { toRouteSegments, toSlug } from '@/lib/util'; import styles from './page.module.css'; @@ -139,7 +140,7 @@ const Page = async ({ params }: PageProps) => { Request - + Response diff --git a/src/components/ApiRequest/ApiRequest.module.css b/src/components/ApiRequest/ApiRequest.module.css index 9daaba93..85f7b308 100644 --- a/src/components/ApiRequest/ApiRequest.module.css +++ b/src/components/ApiRequest/ApiRequest.module.css @@ -1,10 +1,28 @@ .request { grid-column: 1 / -1; + margin-block-end: var(--column-block-padding); +} + +.sandbox { + display: inline-flex; + border: 1px solid var(--color-border); + border-radius: var(--border-radius-m); + padding: var(--space-2xs) var(--space-xs); + align-items: center; + gap: var(--space-xs); } .url { - display: flex; + display: inline-flex; column-gap: var(--space-xs); - margin-block-end: var(--column-block-padding); + word-break: break-all; + align-items: center; + border: 1px solid var(--color-border); + border-radius: var(--border-radius-m); + padding: var(--space-2xs) var(--space-xs); +} + +.method { + align-self: center; } diff --git a/src/components/ApiRequest/ApiRequest.tsx b/src/components/ApiRequest/ApiRequest.tsx index fb404797..af731119 100644 --- a/src/components/ApiRequest/ApiRequest.tsx +++ b/src/components/ApiRequest/ApiRequest.tsx @@ -1,21 +1,38 @@ -import { Tag } from '@/components'; +import { Button, Tag } from '@/components'; import { ApiOperation } from '@/lib/swagger/types'; +import { operationUrl } from '@/lib/url'; import { ApiGrid, ApiGridColumn, ApiGridRow } from '../ApiGrid'; import { ApiMediaResponse } from '../ApiMedia'; +import { ApiSandbox } from '../ApiSandbox'; +import { ClipboardCopy } from '../ClipboardCopy/ClipboardCopy'; import styles from './ApiRequest.module.css'; -export const ApiRequest = (operation: ApiOperation) => { +export const ApiRequest = ({ + operation, + operations, +}: { + operation: ApiOperation; + operations: ApiOperation[]; +}) => { const getParametersByParam = (param: string) => operation.parameters?.filter((p) => p.in === param); const pathsParameters = getParametersByParam('path'); const queryParameters = getParametersByParam('query'); + const url = operationUrl(operation); + return ( <>
-
- - {`${process.env.NEXT_PUBLIC_CLOUDSMITH_API_URL}/${operation.version}${operation.path}`} +
+ + + {url} + + +
From 479fd27e0536eda66e0fd4ec35609746ab486b94 Mon Sep 17 00:00:00 2001 From: Fernando Florenzano Date: Mon, 19 Jan 2026 17:05:11 -0300 Subject: [PATCH 05/11] progress with base dialog and selector with combobox --- package-lock.json | 701 +++++++++++++++++- package.json | 4 +- pnpm-lock.yaml | 295 +++++++- src/app/_styles/variables.css | 2 +- .../ApiRequest/ApiRequest.module.css | 13 +- src/components/ApiRequest/ApiRequest.tsx | 18 +- .../ApiSandbox/ApiSandbox.module.css | 62 ++ src/components/ApiSandbox/ApiSandbox.tsx | 61 ++ .../SandboxInput/SandboxInput.module.css | 37 + .../ApiSandbox/SandboxInput/SandboxInput.tsx | 45 ++ .../ApiSandbox/SandboxInput/components.tsx | 48 ++ .../OperationSelect.module.css | 111 +++ .../OperationSelect/OperationSelect.tsx | 128 ++++ .../components/OperationSelect/index.ts | 3 + .../ApiSandbox/SandboxInput/index.ts | 3 + .../ApiSandbox/SandboxOutput.module.css | 20 + src/components/ApiSandbox/SandboxOutput.tsx | 24 + src/components/ApiSandbox/index.ts | 1 + .../ClipboardCopy/ClipboardCopy.module.css | 10 + src/lib/swagger/util.ts | 7 + src/lib/url.ts | 4 + 21 files changed, 1546 insertions(+), 51 deletions(-) create mode 100644 src/components/ApiSandbox/ApiSandbox.module.css create mode 100644 src/components/ApiSandbox/ApiSandbox.tsx create mode 100644 src/components/ApiSandbox/SandboxInput/SandboxInput.module.css create mode 100644 src/components/ApiSandbox/SandboxInput/SandboxInput.tsx create mode 100644 src/components/ApiSandbox/SandboxInput/components.tsx create mode 100644 src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.module.css create mode 100644 src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.tsx create mode 100644 src/components/ApiSandbox/SandboxInput/components/OperationSelect/index.ts create mode 100644 src/components/ApiSandbox/SandboxInput/index.ts create mode 100644 src/components/ApiSandbox/SandboxOutput.module.css create mode 100644 src/components/ApiSandbox/SandboxOutput.tsx create mode 100644 src/components/ApiSandbox/index.ts create mode 100644 src/lib/url.ts diff --git a/package-lock.json b/package-lock.json index 3db59545..a7783ced 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", "@next/mdx": "15.1.6", + "@vercel/analytics": "^1.6.1", "class-variance-authority": "^0.7.1", "date-fns": "^4.1.0", "fast-fuzzy": "^1.12.0", @@ -26,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", @@ -140,6 +143,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", @@ -3808,6 +3852,48 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "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", @@ -5256,6 +5342,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", @@ -5263,6 +5356,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", @@ -5323,26 +5483,26 @@ } }, "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": "*", @@ -5359,6 +5519,49 @@ } } }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", @@ -5376,15 +5579,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" }, @@ -5403,6 +5606,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", @@ -5420,14 +5666,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": { @@ -5445,6 +5691,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", @@ -5464,14 +5753,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": { @@ -5489,6 +5854,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", @@ -5570,6 +5978,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", @@ -5690,6 +6236,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", @@ -5714,6 +6314,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", @@ -6989,6 +7596,44 @@ "win32" ] }, + "node_modules/@vercel/analytics": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-1.6.1.tgz", + "integrity": "sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==", + "license": "MPL-2.0", + "peerDependencies": { + "@remix-run/react": "^2", + "@sveltejs/kit": "^1 || ^2", + "next": ">= 13", + "react": "^18 || ^19 || ^19.0.0-rc", + "svelte": ">= 4", + "vue": "^3", + "vue-router": "^4" + }, + "peerDependenciesMeta": { + "@remix-run/react": { + "optional": true + }, + "@sveltejs/kit": { + "optional": true + }, + "next": { + "optional": true + }, + "react": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + }, + "vue-router": { + "optional": true + } + } + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -18358,6 +19003,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 3cfccdae..df999cad 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/pnpm-lock.yaml b/pnpm-lock.yaml index 5a5db648..f7a85dd7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,9 @@ importers: specifier: ^5.0.0 version: 5.0.0 devDependencies: + '@ariakit/react': + specifier: ^0.4.21 + version: 0.4.21(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@csstools/postcss-global-data': specifier: ^3.0.0 version: 3.0.0(postcss@8.5.2) @@ -73,8 +76,11 @@ importers: specifier: 15.1.7 version: 15.1.7 '@radix-ui/react-dialog': - specifier: ^1.1.5 - version: 1.1.5(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + specifier: 1.1.4 + version: 1.1.4(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-select': + specifier: 2.1.4 + version: 2.1.4(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@radix-ui/react-tabs': specifier: ^1.1.3 version: 1.1.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -199,6 +205,21 @@ packages: peerDependencies: openapi-types: '>=7' + '@ariakit/core@0.4.18': + resolution: {integrity: sha512-9urEa+GbZTSyredq3B/3thQjTcSZSUC68XctwCkJNH/xNfKN5O+VThiem2rcJxpsGw8sRUQenhagZi0yB4foyg==} + + '@ariakit/react-core@0.4.21': + resolution: {integrity: sha512-rUI9uB/gT3mROFja/ka7/JukkdljIZR3eq3BGiQqX4Ni/KBMDvPK8FvVLnC0TGzWcqNY2bbfve8QllvHzuw4fQ==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@ariakit/react@0.4.21': + resolution: {integrity: sha512-UjP99Y7cWxA5seRECEE0RPZFImkLGFIWPflp65t0BVZwlMw4wp9OJZRHMrnkEkKl5KBE2NR/gbbzwHc6VNGzsA==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -1283,6 +1304,21 @@ packages: resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/react-dom@2.1.6': + resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1666,9 +1702,38 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@radix-ui/number@1.1.0': + resolution: {integrity: sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==} + '@radix-ui/primitive@1.1.1': resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + '@radix-ui/react-arrow@1.1.1': + resolution: {integrity: sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==} + 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 + + '@radix-ui/react-collection@1.1.1': + resolution: {integrity: sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==} + 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 + '@radix-ui/react-collection@1.1.2': resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==} peerDependencies: @@ -1700,8 +1765,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-dialog@1.1.5': - resolution: {integrity: sha512-LaO3e5h/NOEL4OfXjxD43k9Dx+vn+8n+PCFt6uhX/BADFflllyv3WJG6rgvvSVBxpTch938Qq/LGc2MMxipXPw==} + '@radix-ui/react-dialog@1.1.4': + resolution: {integrity: sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1722,8 +1787,8 @@ packages: '@types/react': optional: true - '@radix-ui/react-dismissable-layer@1.1.4': - resolution: {integrity: sha512-XDUI0IVYVSwjMXxM6P4Dfti7AH+Y4oS/TB+sglZ/EXc7cqLwGAmp1NlMrcUjj7ks6R5WTZuWKv44FBbLpwU3sA==} + '@radix-ui/react-dismissable-layer@1.1.3': + resolution: {integrity: sha512-onrWn/72lQoEucDmJnr8uczSNTujT0vJnA/X5+3AkChVPowr8n1yvIKIabhWyMQeMvvmdpsvcyDqx3X1LEXCPg==} peerDependencies: '@types/react': '*' '@types/react-dom': '*' @@ -1766,6 +1831,19 @@ packages: '@types/react': optional: true + '@radix-ui/react-popper@1.2.1': + resolution: {integrity: sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==} + 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 + '@radix-ui/react-portal@1.1.3': resolution: {integrity: sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==} peerDependencies: @@ -1831,6 +1909,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-select@2.1.4': + resolution: {integrity: sha512-pOkb2u8KgO47j/h7AylCj7dJsm69BXcjkrvTqMptFqsE2i0p8lHkfgneXKjAgPzBMivnoMyt8o4KiV4wYzDdyQ==} + 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 + '@radix-ui/react-slot@1.1.1': resolution: {integrity: sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==} peerDependencies: @@ -1898,6 +1989,46 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-previous@1.1.0': + resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.0': + resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.0': + resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.1.1': + resolution: {integrity: sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==} + 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 + '@radix-ui/react-visually-hidden@1.1.2': resolution: {integrity: sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==} peerDependencies: @@ -1911,6 +2042,9 @@ packages: '@types/react-dom': optional: true + '@radix-ui/rect@1.1.0': + resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} @@ -5268,6 +5402,11 @@ packages: '@types/react': optional: true + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -5431,6 +5570,22 @@ snapshots: call-me-maybe: 1.0.2 openapi-types: 12.1.3 + '@ariakit/core@0.4.18': {} + + '@ariakit/react-core@0.4.21(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + '@ariakit/core': 0.4.18 + '@floating-ui/dom': 1.7.4 + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + use-sync-external-store: 1.6.0(react@19.2.1) + + '@ariakit/react@0.4.21(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + '@ariakit/react-core': 0.4.21(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + '@babel/code-frame@7.26.2': dependencies: '@babel/helper-validator-identifier': 7.25.9 @@ -6618,6 +6773,23 @@ snapshots: '@eslint/core': 0.15.2 levn: 0.4.1 + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + '@floating-ui/dom': 1.7.4 + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + + '@floating-ui/utils@0.2.10': {} + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -7039,8 +7211,31 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@radix-ui/number@1.1.0': {} + '@radix-ui/primitive@1.1.1': {} + '@radix-ui/react-arrow@1.1.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + optionalDependencies: + '@types/react': 19.0.8 + '@types/react-dom': 19.0.3(@types/react@19.0.8) + + '@radix-ui/react-collection@1.1.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.2.1) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.2.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-slot': 1.1.1(@types/react@19.0.8)(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + optionalDependencies: + '@types/react': 19.0.8 + '@types/react-dom': 19.0.3(@types/react@19.0.8) + '@radix-ui/react-collection@1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.2.1) @@ -7065,12 +7260,12 @@ snapshots: optionalDependencies: '@types/react': 19.0.8 - '@radix-ui/react-dialog@1.1.5(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + '@radix-ui/react-dialog@1.1.4(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/primitive': 1.1.1 '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.2.1) '@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.2.1) - '@radix-ui/react-dismissable-layer': 1.1.4(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.8)(react@19.2.1) '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) '@radix-ui/react-id': 1.1.0(@types/react@19.0.8)(react@19.2.1) @@ -7093,7 +7288,7 @@ snapshots: optionalDependencies: '@types/react': 19.0.8 - '@radix-ui/react-dismissable-layer@1.1.4(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + '@radix-ui/react-dismissable-layer@1.1.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/primitive': 1.1.1 '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.2.1) @@ -7130,6 +7325,24 @@ snapshots: optionalDependencies: '@types/react': 19.0.8 + '@radix-ui/react-popper@1.2.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + '@floating-ui/react-dom': 2.1.6(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-arrow': 1.1.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.2.1) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.2.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.8)(react@19.2.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.8)(react@19.2.1) + '@radix-ui/react-use-rect': 1.1.0(@types/react@19.0.8)(react@19.2.1) + '@radix-ui/react-use-size': 1.1.0(@types/react@19.0.8)(react@19.2.1) + '@radix-ui/rect': 1.1.0 + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + optionalDependencies: + '@types/react': 19.0.8 + '@types/react-dom': 19.0.3(@types/react@19.0.8) + '@radix-ui/react-portal@1.1.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -7185,6 +7398,35 @@ snapshots: '@types/react': 19.0.8 '@types/react-dom': 19.0.3(@types/react@19.0.8) + '@radix-ui/react-select@2.1.4(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + '@radix-ui/number': 1.1.0 + '@radix-ui/primitive': 1.1.1 + '@radix-ui/react-collection': 1.1.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.2.1) + '@radix-ui/react-context': 1.1.1(@types/react@19.0.8)(react@19.2.1) + '@radix-ui/react-direction': 1.1.0(@types/react@19.0.8)(react@19.2.1) + '@radix-ui/react-dismissable-layer': 1.1.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@19.0.8)(react@19.2.1) + '@radix-ui/react-focus-scope': 1.1.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-id': 1.1.0(@types/react@19.0.8)(react@19.2.1) + '@radix-ui/react-popper': 1.2.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-portal': 1.1.3(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + '@radix-ui/react-slot': 1.1.1(@types/react@19.0.8)(react@19.2.1) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.8)(react@19.2.1) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@19.0.8)(react@19.2.1) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.8)(react@19.2.1) + '@radix-ui/react-use-previous': 1.1.0(@types/react@19.0.8)(react@19.2.1) + '@radix-ui/react-visually-hidden': 1.1.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + aria-hidden: 1.2.4 + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + react-remove-scroll: 2.6.3(@types/react@19.0.8)(react@19.2.1) + optionalDependencies: + '@types/react': 19.0.8 + '@types/react-dom': 19.0.3(@types/react@19.0.8) + '@radix-ui/react-slot@1.1.1(@types/react@19.0.8)(react@19.2.1)': dependencies: '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.2.1) @@ -7241,6 +7483,35 @@ snapshots: optionalDependencies: '@types/react': 19.0.8 + '@radix-ui/react-use-previous@1.1.0(@types/react@19.0.8)(react@19.2.1)': + dependencies: + react: 19.2.1 + optionalDependencies: + '@types/react': 19.0.8 + + '@radix-ui/react-use-rect@1.1.0(@types/react@19.0.8)(react@19.2.1)': + dependencies: + '@radix-ui/rect': 1.1.0 + react: 19.2.1 + optionalDependencies: + '@types/react': 19.0.8 + + '@radix-ui/react-use-size@1.1.0(@types/react@19.0.8)(react@19.2.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.8)(react@19.2.1) + react: 19.2.1 + optionalDependencies: + '@types/react': 19.0.8 + + '@radix-ui/react-visually-hidden@1.1.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': + dependencies: + '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) + react: 19.2.1 + react-dom: 19.2.1(react@19.2.1) + optionalDependencies: + '@types/react': 19.0.8 + '@types/react-dom': 19.0.3(@types/react@19.0.8) + '@radix-ui/react-visually-hidden@1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1)': dependencies: '@radix-ui/react-primitive': 2.0.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.2.1(react@19.2.1))(react@19.2.1) @@ -7250,6 +7521,8 @@ snapshots: '@types/react': 19.0.8 '@types/react-dom': 19.0.3(@types/react@19.0.8) + '@radix-ui/rect@1.1.0': {} + '@rtsao/scc@1.1.0': {} '@rushstack/eslint-patch@1.10.5': {} @@ -11683,6 +11956,10 @@ snapshots: optionalDependencies: '@types/react': 19.0.8 + use-sync-external-store@1.6.0(react@19.2.1): + dependencies: + react: 19.2.1 + util-deprecate@1.0.2: {} v8-compile-cache-lib@3.0.1: {} 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 85f7b308..f7a095c6 100644 --- a/src/components/ApiRequest/ApiRequest.module.css +++ b/src/components/ApiRequest/ApiRequest.module.css @@ -5,24 +5,21 @@ .sandbox { display: inline-flex; - border: 1px solid var(--color-border); - border-radius: var(--border-radius-m); - padding: var(--space-2xs) var(--space-xs); align-items: center; gap: var(--space-xs); } -.url { +.urlCopy { display: inline-flex; column-gap: var(--space-xs); word-break: break-all; - align-items: center; border: 1px solid var(--color-border); border-radius: var(--border-radius-m); - padding: var(--space-2xs) var(--space-xs); + padding: var(--space-3xs) var(--space-2xs); } -.method { - align-self: center; +.url { + text-align: start; + padding-top: var(--space-5xs); } diff --git a/src/components/ApiRequest/ApiRequest.tsx b/src/components/ApiRequest/ApiRequest.tsx index af731119..72e1f40e 100644 --- a/src/components/ApiRequest/ApiRequest.tsx +++ b/src/components/ApiRequest/ApiRequest.tsx @@ -1,5 +1,8 @@ -import { Button, Tag } from '@/components'; +import { cx } from 'class-variance-authority'; + +import { Tag } from '@/components'; import { ApiOperation } from '@/lib/swagger/types'; +import { getParametersByParam } from '@/lib/swagger/util'; import { operationUrl } from '@/lib/url'; import { ApiGrid, ApiGridColumn, ApiGridRow } from '../ApiGrid'; @@ -15,9 +18,8 @@ export const ApiRequest = ({ operation: ApiOperation; operations: ApiOperation[]; }) => { - const getParametersByParam = (param: string) => operation.parameters?.filter((p) => p.in === param); - const pathsParameters = getParametersByParam('path'); - const queryParameters = getParametersByParam('query'); + const pathsParameters = getParametersByParam(operation, 'path'); + const queryParameters = getParametersByParam(operation, 'query'); const url = operationUrl(operation); @@ -25,14 +27,12 @@ export const ApiRequest = ({ <>
- + - {url} + {url} - +
diff --git a/src/components/ApiSandbox/ApiSandbox.module.css b/src/components/ApiSandbox/ApiSandbox.module.css new file mode 100644 index 00000000..5e6c5646 --- /dev/null +++ b/src/components/ApiSandbox/ApiSandbox.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/ApiSandbox.tsx b/src/components/ApiSandbox/ApiSandbox.tsx new file mode 100644 index 00000000..3d9b42bb --- /dev/null +++ b/src/components/ApiSandbox/ApiSandbox.tsx @@ -0,0 +1,61 @@ +'use client'; + +import { useEffect, useState } from 'react'; + +import * as RadixDialog from '@radix-ui/react-dialog'; +import { VisuallyHidden } from '@radix-ui/react-visually-hidden'; + +import { ApiOperation } from '@/lib/swagger/types'; + +import { Button } from '../Button'; +import styles from './ApiSandbox.module.css'; +import { SandboxInput } from './SandboxInput/SandboxInput'; +import { SandboxOutput } from './SandboxOutput'; + +type ApiSandboxProps = { + operation: ApiOperation; + operations: ApiOperation[]; +}; + +export const ApiSandbox = ({ operation, operations }: ApiSandboxProps) => { + 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/SandboxInput/SandboxInput.module.css b/src/components/ApiSandbox/SandboxInput/SandboxInput.module.css new file mode 100644 index 00000000..f52b3ed8 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/SandboxInput.module.css @@ -0,0 +1,37 @@ +.root { + min-width: 400px; + padding: var(--space-m); + max-height: calc(100vh - 50px); + overflow-y: auto; +} + +@media (--phablet-up) { + .root { + max-height: calc(100vh - 150px); + } +} + +.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-s); + + 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..58d9affe --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/SandboxInput.tsx @@ -0,0 +1,45 @@ +import { cx } from 'class-variance-authority'; + +import { Flex } from '@/components/Flex'; +import { ApiOperation } from '@/lib/swagger/types'; +import { getParametersByParam } from '@/lib/swagger/util'; +import { operationUrl } from '@/lib/url'; + +import { ClipboardCopy } from '../../ClipboardCopy/ClipboardCopy'; +import { Tag } from '../../Tag'; +import { PathParams, QueryParams, RequestBody } from './components'; +import OperationSelect from './components/OperationSelect'; +import styles from './SandboxInput.module.css'; + +type SandboxInputProps = { + operation: ApiOperation; + operations: ApiOperation[]; + onChangeOperation: (o: ApiOperation) => void; +}; + +export const SandboxInput = ({ operation, operations, onChangeOperation }: SandboxInputProps) => { + const pathsParameters = getParametersByParam(operation, 'path'); + const queryParameters = getParametersByParam(operation, 'query'); + const bodyParameters = operation.requestBody; + + const url = operationUrl(operation); + + return ( + + + + + + {url} + + +
+ {pathsParameters?.length ? : null} + + {queryParameters?.length ? : null} + + {bodyParameters ? : null} +
+
+ ); +}; diff --git a/src/components/ApiSandbox/SandboxInput/components.tsx b/src/components/ApiSandbox/SandboxInput/components.tsx new file mode 100644 index 00000000..a5bdcc61 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components.tsx @@ -0,0 +1,48 @@ +import { ApiGrid, ApiGridColumn, ApiGridRow } from '@/components/ApiGrid'; +import { ApiMediaResponse } from '@/components/ApiMedia'; +import { Tag } from '@/components/Tag'; +import { ApiOperation } from '@/lib/swagger/types'; + +export const PathParams = ({ parameters }: { parameters: NonNullable }) => ( + + {parameters.map((param) => ( + + {param.name} + {param.schema?.type} + + {param.required ? 'required' : 'optional'} + + + ))} + +); + +export const QueryParams = ({ parameters }: { parameters: NonNullable }) => ( + + {parameters.map((param) => ( + + {param.name} + {param.schema?.type} + {param.description} + + ))} + +); + +export const RequestBody = ({ requestBody }: { requestBody: NonNullable }) => ( + + {'Body params '} + + {requestBody.required ? 'required' : 'optional'} + + + }> + + + + + + +); 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..6bc5f5dd --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.module.css @@ -0,0 +1,111 @@ +.trigger { + padding: var(--space-2xs); + cursor: pointer; + border-radius: var(--border-radius-s); +} + +.trigger h2 { + margin: 0; +} + +.trigger[aria-expanded='true'] { + outline: solid 1px var(--color-accent-default); +} + +.content { + 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-4xs); + 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..74755f5c --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.tsx @@ -0,0 +1,128 @@ +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 { ApiOperation } from '@/lib/swagger/types'; +import { operationKey } from '@/lib/swagger/util'; + +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/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.module.css b/src/components/ApiSandbox/SandboxOutput.module.css new file mode 100644 index 00000000..05b8e670 --- /dev/null +++ b/src/components/ApiSandbox/SandboxOutput.module.css @@ -0,0 +1,20 @@ +.root { + background-color: var(--color-background-pre); + display: flex; + flex-direction: column; + padding: var(--space-m); + gap: var(--space-m); + min-width: 400px; +} + +.header { + color: var(--color-text-on-color); + display: flex; + justify-content: space-between; + align-items: center; +} + +.request, +.response { + /* background-color: var(--base-color-grey-1000); */ +} diff --git a/src/components/ApiSandbox/SandboxOutput.tsx b/src/components/ApiSandbox/SandboxOutput.tsx new file mode 100644 index 00000000..19b17aba --- /dev/null +++ b/src/components/ApiSandbox/SandboxOutput.tsx @@ -0,0 +1,24 @@ +// import { Suspense } from 'react'; + +import { Button } from '../Button'; +// import { CodeBlock } from '../CodeBlock'; +import { Paragraph } from '../Paragraph'; +import styles from './SandboxOutput.module.css'; + +export const SandboxOutput = () => { + return ( +
+
+ cURL Request + + +
+ + {/* + curl --request GET + */} + + {/*
hola
*/} +
+ ); +}; diff --git a/src/components/ApiSandbox/index.ts b/src/components/ApiSandbox/index.ts new file mode 100644 index 00000000..80f007e6 --- /dev/null +++ b/src/components/ApiSandbox/index.ts @@ -0,0 +1 @@ +export * from './ApiSandbox'; diff --git a/src/components/ClipboardCopy/ClipboardCopy.module.css b/src/components/ClipboardCopy/ClipboardCopy.module.css index d34d9df9..ead95c93 100644 --- a/src/components/ClipboardCopy/ClipboardCopy.module.css +++ b/src/components/ClipboardCopy/ClipboardCopy.module.css @@ -13,6 +13,16 @@ 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); diff --git a/src/lib/swagger/util.ts b/src/lib/swagger/util.ts index 2e7db9b9..947da435 100644 --- a/src/lib/swagger/util.ts +++ b/src/lib/swagger/util.ts @@ -1,5 +1,7 @@ import { OpenAPIV3 } from 'openapi-types'; + import { replaceAll, titleCase } from '../util'; +import { ApiOperation } from './types'; export const isHttpMethod = (method: string): boolean => Object.values(OpenAPIV3.HttpMethods).includes(method); @@ -49,3 +51,8 @@ export const createTitle = (menuSegments: string[]): string => { export const apiOperationPath = (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}`; diff --git a/src/lib/url.ts b/src/lib/url.ts new file mode 100644 index 00000000..65b387ec --- /dev/null +++ b/src/lib/url.ts @@ -0,0 +1,4 @@ +import { ApiOperation } from './swagger/types'; + +export const operationUrl = (operation: ApiOperation) => + `${process.env.NEXT_PUBLIC_CLOUDSMITH_API_URL}/${operation.version}${operation.path}`; From 1c3171be7a9157e3f8d68dd52c6b5fabc9582db8 Mon Sep 17 00:00:00 2001 From: Fernando Florenzano Date: Tue, 20 Jan 2026 09:27:41 -0300 Subject: [PATCH 06/11] tweak item indicator position --- .../components/OperationSelect/OperationSelect.module.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.module.css b/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.module.css index 6bc5f5dd..8c31a94c 100644 --- a/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.module.css +++ b/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.module.css @@ -94,7 +94,8 @@ .itemIndicator { position: absolute; - left: var(--space-4xs); + left: var(--space-3xs); + top: var(--space-s); color: var(--color-accent-default); } From 952c572ee3873fbd1e7dc0dd9e56d9682803d908 Mon Sep 17 00:00:00 2001 From: Fernando Florenzano Date: Wed, 21 Jan 2026 15:40:02 -0300 Subject: [PATCH 07/11] chore: general layout and component organization --- src/components/ApiRequest/ApiRequest.tsx | 2 +- src/components/ApiSandbox/ApiSandbox.tsx | 6 +- .../SandboxInput/SandboxInput.module.css | 12 ++- .../ApiSandbox/SandboxInput/SandboxInput.tsx | 10 +- .../ApiSandbox/SandboxInput/components.tsx | 48 ---------- .../OperationSelect.module.css | 3 +- .../OperationSelect/OperationSelect.tsx | 5 +- .../PathParams/PathParams.module.css | 40 ++++++++ .../components/PathParams/PathParams.tsx | 28 ++++++ .../components/PathParams/index.ts | 3 + .../QueryParams/QueryParams.module.css | 40 ++++++++ .../components/QueryParams/QueryParams.tsx | 28 ++++++ .../components/QueryParams/index.ts | 3 + .../RequestBody/RequestBody.module.css | 40 ++++++++ .../components/RequestBody/RequestBody.tsx | 55 +++++++++++ .../components/RequestBody/index.ts | 3 + .../ApiSandbox/SandboxOutput.module.css | 20 ---- src/components/ApiSandbox/SandboxOutput.tsx | 24 ----- .../SandboxOutput/SandboxOutput.module.css | 43 +++++++++ .../SandboxOutput/SandboxOutput.tsx | 93 +++++++++++++++++++ .../ApiSandbox/SandboxOutput/index.ts | 3 + src/components/CodeBlock/CodeBlock.module.css | 23 ++++- src/components/CodeBlock/CodeBlock.tsx | 29 +++--- src/components/CodeBlock/CodeBlockSync.tsx | 66 +++++++++++++ src/components/CodeBlock/index.ts | 2 +- src/components/CodeBlock/props.ts | 10 ++ src/lib/highlight/client.ts | 28 ++++++ .../{highlight.tsx => highlight/server.ts} | 11 ++- src/lib/highlight/theme.ts | 1 + src/lib/operations/util.ts | 30 ++++++ src/lib/search/server.ts | 11 ++- src/lib/swagger/parse.ts | 7 +- src/lib/swagger/util.ts | 13 --- 33 files changed, 590 insertions(+), 150 deletions(-) delete mode 100644 src/components/ApiSandbox/SandboxInput/components.tsx create mode 100644 src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.module.css create mode 100644 src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.tsx create mode 100644 src/components/ApiSandbox/SandboxInput/components/PathParams/index.ts create mode 100644 src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.module.css create mode 100644 src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.tsx create mode 100644 src/components/ApiSandbox/SandboxInput/components/QueryParams/index.ts create mode 100644 src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.module.css create mode 100644 src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.tsx create mode 100644 src/components/ApiSandbox/SandboxInput/components/RequestBody/index.ts delete mode 100644 src/components/ApiSandbox/SandboxOutput.module.css delete mode 100644 src/components/ApiSandbox/SandboxOutput.tsx create mode 100644 src/components/ApiSandbox/SandboxOutput/SandboxOutput.module.css create mode 100644 src/components/ApiSandbox/SandboxOutput/SandboxOutput.tsx create mode 100644 src/components/ApiSandbox/SandboxOutput/index.ts create mode 100644 src/components/CodeBlock/CodeBlockSync.tsx create mode 100644 src/components/CodeBlock/props.ts create mode 100644 src/lib/highlight/client.ts rename src/lib/{highlight.tsx => highlight/server.ts} (73%) create mode 100644 src/lib/highlight/theme.ts create mode 100644 src/lib/operations/util.ts diff --git a/src/components/ApiRequest/ApiRequest.tsx b/src/components/ApiRequest/ApiRequest.tsx index 72e1f40e..a36d6f69 100644 --- a/src/components/ApiRequest/ApiRequest.tsx +++ b/src/components/ApiRequest/ApiRequest.tsx @@ -1,8 +1,8 @@ import { cx } from 'class-variance-authority'; import { Tag } from '@/components'; +import { getParametersByParam } from '@/lib/operations/util'; import { ApiOperation } from '@/lib/swagger/types'; -import { getParametersByParam } from '@/lib/swagger/util'; import { operationUrl } from '@/lib/url'; import { ApiGrid, ApiGridColumn, ApiGridRow } from '../ApiGrid'; diff --git a/src/components/ApiSandbox/ApiSandbox.tsx b/src/components/ApiSandbox/ApiSandbox.tsx index 3d9b42bb..702553ad 100644 --- a/src/components/ApiSandbox/ApiSandbox.tsx +++ b/src/components/ApiSandbox/ApiSandbox.tsx @@ -9,8 +9,8 @@ import { ApiOperation } from '@/lib/swagger/types'; import { Button } from '../Button'; import styles from './ApiSandbox.module.css'; -import { SandboxInput } from './SandboxInput/SandboxInput'; -import { SandboxOutput } from './SandboxOutput'; +import SandboxInput from './SandboxInput'; +import SandboxOutput from './SandboxOutput'; type ApiSandboxProps = { operation: ApiOperation; @@ -51,7 +51,7 @@ export const ApiSandbox = ({ operation, operations }: ApiSandboxProps) => { operations={operations} onChangeOperation={(o) => setCurrentOperation(o)} /> - +
diff --git a/src/components/ApiSandbox/SandboxInput/SandboxInput.module.css b/src/components/ApiSandbox/SandboxInput/SandboxInput.module.css index f52b3ed8..3706c7dd 100644 --- a/src/components/ApiSandbox/SandboxInput/SandboxInput.module.css +++ b/src/components/ApiSandbox/SandboxInput/SandboxInput.module.css @@ -1,13 +1,13 @@ .root { min-width: 400px; padding: var(--space-m); - max-height: calc(100vh - 50px); + max-height: calc(100vh - var(--space-l) * 2); overflow-y: auto; } @media (--phablet-up) { .root { - max-height: calc(100vh - 150px); + max-height: calc(100vh - var(--space-l) * 2); } } @@ -30,8 +30,10 @@ .params { --column-inline-padding: var(--space-m); - --column-block-padding: var(--space-s); + --column-block-padding: var(--space-m); - display: grid; - grid-template-columns: 1fr 1fr 1fr 1fr; + 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 index 58d9affe..2c09d9ef 100644 --- a/src/components/ApiSandbox/SandboxInput/SandboxInput.tsx +++ b/src/components/ApiSandbox/SandboxInput/SandboxInput.tsx @@ -1,14 +1,16 @@ import { cx } from 'class-variance-authority'; +import { ClipboardCopy } from '@/components/ClipboardCopy/ClipboardCopy'; import { Flex } from '@/components/Flex'; +import { Tag } from '@/components/Tag'; +import { getParametersByParam } from '@/lib/operations/util'; import { ApiOperation } from '@/lib/swagger/types'; -import { getParametersByParam } from '@/lib/swagger/util'; import { operationUrl } from '@/lib/url'; -import { ClipboardCopy } from '../../ClipboardCopy/ClipboardCopy'; -import { Tag } from '../../Tag'; -import { PathParams, QueryParams, RequestBody } from './components'; 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 = { diff --git a/src/components/ApiSandbox/SandboxInput/components.tsx b/src/components/ApiSandbox/SandboxInput/components.tsx deleted file mode 100644 index a5bdcc61..00000000 --- a/src/components/ApiSandbox/SandboxInput/components.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { ApiGrid, ApiGridColumn, ApiGridRow } from '@/components/ApiGrid'; -import { ApiMediaResponse } from '@/components/ApiMedia'; -import { Tag } from '@/components/Tag'; -import { ApiOperation } from '@/lib/swagger/types'; - -export const PathParams = ({ parameters }: { parameters: NonNullable }) => ( - - {parameters.map((param) => ( - - {param.name} - {param.schema?.type} - - {param.required ? 'required' : 'optional'} - - - ))} - -); - -export const QueryParams = ({ parameters }: { parameters: NonNullable }) => ( - - {parameters.map((param) => ( - - {param.name} - {param.schema?.type} - {param.description} - - ))} - -); - -export const RequestBody = ({ requestBody }: { requestBody: NonNullable }) => ( - - {'Body params '} - - {requestBody.required ? 'required' : 'optional'} - - - }> - - - - - - -); diff --git a/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.module.css b/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.module.css index 8c31a94c..54f28621 100644 --- a/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.module.css +++ b/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.module.css @@ -1,7 +1,7 @@ .trigger { padding: var(--space-2xs); cursor: pointer; - border-radius: var(--border-radius-s); + border-radius: var(--border-radius-m); } .trigger h2 { @@ -13,6 +13,7 @@ } .content { + min-width: 430px; overflow: hidden; background-color: var(--color-background-default); border-radius: var(--border-radius-m); diff --git a/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.tsx b/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.tsx index 74755f5c..118d20a6 100644 --- a/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.tsx +++ b/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.tsx @@ -7,8 +7,8 @@ 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 { operationKey } from '@/lib/swagger/util'; import styles from './OperationSelect.module.css'; @@ -75,8 +75,7 @@ export default function OperationSelect({ value, options, onValueChange }: Opera aria-label="Operations" position="popper" className={styles.content} - sideOffset={4} - alignOffset={-16}> + sideOffset={4}>
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..20c8ea80 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.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/PathParams/PathParams.tsx b/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.tsx new file mode 100644 index 00000000..960949e6 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.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 './PathParams.module.css'; + +const PathParams = ({ parameters }: { parameters: NonNullable }) => ( +
+
+
Path params
+
+ {parameters.map((param) => ( +
+ +
{param.name}
+
{param.schema?.type}
+
+ {param.required ? 'required' : 'optional'} +
+
+
+ ))} +
+); + +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/SandboxOutput.module.css b/src/components/ApiSandbox/SandboxOutput.module.css deleted file mode 100644 index 05b8e670..00000000 --- a/src/components/ApiSandbox/SandboxOutput.module.css +++ /dev/null @@ -1,20 +0,0 @@ -.root { - background-color: var(--color-background-pre); - display: flex; - flex-direction: column; - padding: var(--space-m); - gap: var(--space-m); - min-width: 400px; -} - -.header { - color: var(--color-text-on-color); - display: flex; - justify-content: space-between; - align-items: center; -} - -.request, -.response { - /* background-color: var(--base-color-grey-1000); */ -} diff --git a/src/components/ApiSandbox/SandboxOutput.tsx b/src/components/ApiSandbox/SandboxOutput.tsx deleted file mode 100644 index 19b17aba..00000000 --- a/src/components/ApiSandbox/SandboxOutput.tsx +++ /dev/null @@ -1,24 +0,0 @@ -// import { Suspense } from 'react'; - -import { Button } from '../Button'; -// import { CodeBlock } from '../CodeBlock'; -import { Paragraph } from '../Paragraph'; -import styles from './SandboxOutput.module.css'; - -export const SandboxOutput = () => { - return ( -
-
- cURL Request - - -
- - {/* - curl --request GET - */} - - {/*
hola
*/} -
- ); -}; 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..8ce00a35 --- /dev/null +++ b/src/components/ApiSandbox/SandboxOutput/SandboxOutput.tsx @@ -0,0 +1,93 @@ +// import { Suspense } from 'react'; + +import { Flex } from '@/components/Flex'; +import { Tag } from '@/components/Tag'; +import { curlCommand } from '@/lib/operations/util'; +import { ApiOperation } from '@/lib/swagger/types'; + +import { Button } from '../../Button'; +import { CodeBlockSync } from '../../CodeBlock/CodeBlockSync'; +import { Paragraph } from '../../Paragraph'; +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; +}; + +export const SandboxOutput = ({ operation }: SandboxOutputProps) => { + const command = curlCommand(operation); + + 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/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 50b63a17..0a84df62 100644 --- a/src/components/CodeBlock/CodeBlock.tsx +++ b/src/components/CodeBlock/CodeBlock.tsx @@ -1,10 +1,12 @@ import { transformerNotationHighlight } from '@shikijs/transformers'; import { cva, cx } from 'class-variance-authority'; -import { getHighlighter, theme } from '@/lib/highlight'; +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: { @@ -14,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, @@ -31,10 +44,10 @@ export async function CodeBlock({ children, lang, header = true }: Props) { }); return ( -
+
{!hideHeader && (
-
{lang}
+
{header ?? lang}
)} @@ -42,9 +55,3 @@ export async function CodeBlock({ children, lang, header = true }: Props) {
); } - -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..f54064da --- /dev/null +++ b/src/lib/operations/util.ts @@ -0,0 +1,30 @@ +import { ApiOperation } from '../swagger/types'; +import { operationUrl } from '../url'; + +/** + * 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, + // pathParams?: any, + // queryParams?: any, + // bodyParams?: any, + // header?: any, +) => { + let command = `curl --request ${op.method.toUpperCase()} \\\n`; + + command += ` --url '${operationUrl(op).replaceAll('\{', '').replaceAll('\}', '')}' \\\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 947da435..5e1e2e2d 100644 --- a/src/lib/swagger/util.ts +++ b/src/lib/swagger/util.ts @@ -1,7 +1,6 @@ import { OpenAPIV3 } from 'openapi-types'; import { replaceAll, titleCase } from '../util'; -import { ApiOperation } from './types'; export const isHttpMethod = (method: string): boolean => Object.values(OpenAPIV3.HttpMethods).includes(method); @@ -44,15 +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}`; -}; - -export const getParametersByParam = (operation: ApiOperation, param: string) => - operation.parameters?.filter((p) => p.in === param); - -export const operationKey = (op: ApiOperation) => `${op.method}-${op.path}`; From 10a97c962c59807077b3ef4ee5f34dd92726ec0d Mon Sep 17 00:00:00 2001 From: Fernando Florenzano Date: Wed, 21 Jan 2026 17:39:15 -0300 Subject: [PATCH 08/11] editable path params --- src/components/ApiRequest/ApiRequest.tsx | 7 +-- ...module.css => ApiSandboxDialog.module.css} | 0 .../{ApiSandbox.tsx => ApiSandboxDialog.tsx} | 16 +++-- src/components/ApiSandbox/Sandbox.tsx | 59 +++++++++++++++++++ .../ApiSandbox/SandboxInput/SandboxInput.tsx | 39 ++++++++---- .../PathParams/PathParams.module.css | 42 ++++++------- .../components/PathParams/PathParams.tsx | 54 ++++++++++++++--- .../SandboxOutput/SandboxOutput.tsx | 17 +++--- src/components/ApiSandbox/index.ts | 2 +- src/lib/operations/util.ts | 26 ++++++-- src/lib/url.ts | 4 -- 11 files changed, 195 insertions(+), 71 deletions(-) rename src/components/ApiSandbox/{ApiSandbox.module.css => ApiSandboxDialog.module.css} (100%) rename src/components/ApiSandbox/{ApiSandbox.tsx => ApiSandboxDialog.tsx} (78%) create mode 100644 src/components/ApiSandbox/Sandbox.tsx diff --git a/src/components/ApiRequest/ApiRequest.tsx b/src/components/ApiRequest/ApiRequest.tsx index a36d6f69..6083fa4d 100644 --- a/src/components/ApiRequest/ApiRequest.tsx +++ b/src/components/ApiRequest/ApiRequest.tsx @@ -1,13 +1,12 @@ import { cx } from 'class-variance-authority'; import { Tag } from '@/components'; -import { getParametersByParam } from '@/lib/operations/util'; +import { getParametersByParam, operationUrl } from '@/lib/operations/util'; import { ApiOperation } from '@/lib/swagger/types'; -import { operationUrl } from '@/lib/url'; import { ApiGrid, ApiGridColumn, ApiGridRow } from '../ApiGrid'; import { ApiMediaResponse } from '../ApiMedia'; -import { ApiSandbox } from '../ApiSandbox'; +import { ApiSandboxDialog } from '../ApiSandbox'; import { ClipboardCopy } from '../ClipboardCopy/ClipboardCopy'; import styles from './ApiRequest.module.css'; @@ -32,7 +31,7 @@ export const ApiRequest = ({ {url} - +
diff --git a/src/components/ApiSandbox/ApiSandbox.module.css b/src/components/ApiSandbox/ApiSandboxDialog.module.css similarity index 100% rename from src/components/ApiSandbox/ApiSandbox.module.css rename to src/components/ApiSandbox/ApiSandboxDialog.module.css diff --git a/src/components/ApiSandbox/ApiSandbox.tsx b/src/components/ApiSandbox/ApiSandboxDialog.tsx similarity index 78% rename from src/components/ApiSandbox/ApiSandbox.tsx rename to src/components/ApiSandbox/ApiSandboxDialog.tsx index 702553ad..75c2a8af 100644 --- a/src/components/ApiSandbox/ApiSandbox.tsx +++ b/src/components/ApiSandbox/ApiSandboxDialog.tsx @@ -5,19 +5,18 @@ 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 { Button } from '../Button'; -import styles from './ApiSandbox.module.css'; -import SandboxInput from './SandboxInput'; -import SandboxOutput from './SandboxOutput'; +import styles from './ApiSandboxDialog.module.css'; +import { Sandbox } from './Sandbox'; -type ApiSandboxProps = { +type ApiSandboxDialogProps = { operation: ApiOperation; operations: ApiOperation[]; }; -export const ApiSandbox = ({ operation, operations }: ApiSandboxProps) => { +export const ApiSandboxDialog = ({ operation, operations }: ApiSandboxDialogProps) => { const [open, setOpen] = useState(false); const [currentOperation, setCurrentOperation] = useState(operation); @@ -46,12 +45,11 @@ export const ApiSandbox = ({ operation, operations }: ApiSandboxProps) => {
- 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.tsx b/src/components/ApiSandbox/SandboxInput/SandboxInput.tsx index 2c09d9ef..114dceb9 100644 --- a/src/components/ApiSandbox/SandboxInput/SandboxInput.tsx +++ b/src/components/ApiSandbox/SandboxInput/SandboxInput.tsx @@ -3,9 +3,8 @@ import { cx } from 'class-variance-authority'; import { ClipboardCopy } from '@/components/ClipboardCopy/ClipboardCopy'; import { Flex } from '@/components/Flex'; import { Tag } from '@/components/Tag'; -import { getParametersByParam } from '@/lib/operations/util'; -import { ApiOperation } from '@/lib/swagger/types'; -import { operationUrl } from '@/lib/url'; +import { operationUrl } from '@/lib/operations/util'; +import { ApiOperation, ParameterObject, RequestBodyObject } from '@/lib/swagger/types'; import OperationSelect from './components/OperationSelect'; import PathParams from './components/PathParams'; @@ -16,13 +15,27 @@ 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 }: SandboxInputProps) => { - const pathsParameters = getParametersByParam(operation, 'path'); - const queryParameters = getParametersByParam(operation, 'query'); - const bodyParameters = operation.requestBody; +export const SandboxInput = ({ + operation, + operations, + onChangeOperation, + parameters, + paramState, + onUpdateState, +}: SandboxInputProps) => { + const { path, query, body } = parameters; const url = operationUrl(operation); @@ -36,11 +49,17 @@ export const SandboxInput = ({ operation, operations, onChangeOperation }: Sandb
- {pathsParameters?.length ? : null} + {path.length > 0 ? ( + onUpdateState('param', name, value)} + /> + ) : null} - {queryParameters?.length ? : null} + {query.length > 0 ? : null} - {bodyParameters ? : null} + {body ? : null}
); diff --git a/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.module.css b/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.module.css index 20c8ea80..c0adbbfc 100644 --- a/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.module.css +++ b/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.module.css @@ -1,39 +1,41 @@ -.grid { +.root { 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); +.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); } -.subItem { +.heading { 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); - } + padding-block: calc(var(--column-block-padding) / 2); } .param { - padding: var(--space-xs) var(--space-m); + border-top: 1px solid var(--color-border); + padding: var(--space-xs) var(--space-xs) 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); +.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); } -.subItemType { +.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 index 960949e6..a6f4f222 100644 --- a/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.tsx +++ b/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.tsx @@ -1,26 +1,62 @@ +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'; -const PathParams = ({ parameters }: { parameters: NonNullable }) => ( -
-
-
Path params
+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.schema?.type}
- {param.required ? 'required' : 'optional'} + {param.required ? 'required' : 'optional'}
-
+ + onUpdateParam(param.name, v)} /> + ))}
); diff --git a/src/components/ApiSandbox/SandboxOutput/SandboxOutput.tsx b/src/components/ApiSandbox/SandboxOutput/SandboxOutput.tsx index 8ce00a35..46bad26a 100644 --- a/src/components/ApiSandbox/SandboxOutput/SandboxOutput.tsx +++ b/src/components/ApiSandbox/SandboxOutput/SandboxOutput.tsx @@ -1,13 +1,11 @@ -// import { Suspense } from 'react'; - +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 { Button } from '../../Button'; -import { CodeBlockSync } from '../../CodeBlock/CodeBlockSync'; -import { Paragraph } from '../../Paragraph'; import styles from './SandboxOutput.module.css'; const response = [ @@ -60,15 +58,18 @@ const response = [ type SandboxOutputProps = { operation: ApiOperation; + paramState: { + path: Record; + }; }; -export const SandboxOutput = ({ operation }: SandboxOutputProps) => { - const command = curlCommand(operation); +export const SandboxOutput = ({ operation, paramState }: SandboxOutputProps) => { + const command = curlCommand(operation, paramState); const stringResponse = JSON.stringify(response, null, 4); return ( - + cURL Request diff --git a/src/components/ApiSandbox/index.ts b/src/components/ApiSandbox/index.ts index 80f007e6..61c4635b 100644 --- a/src/components/ApiSandbox/index.ts +++ b/src/components/ApiSandbox/index.ts @@ -1 +1 @@ -export * from './ApiSandbox'; +export * from './ApiSandboxDialog'; diff --git a/src/lib/operations/util.ts b/src/lib/operations/util.ts index f54064da..c52e6c55 100644 --- a/src/lib/operations/util.ts +++ b/src/lib/operations/util.ts @@ -1,5 +1,7 @@ import { ApiOperation } from '../swagger/types'; -import { operationUrl } from '../url'; + +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 @@ -15,14 +17,26 @@ export const operationKey = (op: ApiOperation) => `${op.method}-${op.path}`; export const curlCommand = ( op: ApiOperation, - // pathParams?: any, - // queryParams?: any, - // bodyParams?: any, - // header?: any, + parameters: { + path: Record; + }, ) => { let command = `curl --request ${op.method.toUpperCase()} \\\n`; - command += ` --url '${operationUrl(op).replaceAll('\{', '').replaceAll('\}', '')}' \\\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'`; diff --git a/src/lib/url.ts b/src/lib/url.ts index 65b387ec..e69de29b 100644 --- a/src/lib/url.ts +++ b/src/lib/url.ts @@ -1,4 +0,0 @@ -import { ApiOperation } from './swagger/types'; - -export const operationUrl = (operation: ApiOperation) => - `${process.env.NEXT_PUBLIC_CLOUDSMITH_API_URL}/${operation.version}${operation.path}`; From ff76fdbe58789e21c627a251d51b4bc71baa5778 Mon Sep 17 00:00:00 2001 From: Fernando Florenzano Date: Fri, 23 Jan 2026 10:09:29 -0300 Subject: [PATCH 09/11] progress with some editable fields and testable API --- .../ApiSandbox/ApiSandboxDialog.tsx | 2 +- src/components/ApiSandbox/Sandbox.tsx | 32 ++++++- .../SandboxInput/SandboxInput.module.css | 4 +- .../ApiSandbox/SandboxInput/SandboxInput.tsx | 25 ++++- .../components/AuthInput/AuthInput.module.css | 51 ++++++++++ .../components/AuthInput/AuthInput.tsx | 66 +++++++++++++ .../components/AuthInput/index.ts | 3 + .../OperationSelect.module.css | 2 +- .../OperationSelect/OperationSelect.tsx | 2 +- .../PathParams/PathParams.module.css | 7 +- .../components/PathParams/PathParams.tsx | 6 +- .../QueryParams/QueryParams.module.css | 47 +++++---- .../components/QueryParams/QueryParams.tsx | 19 ++-- .../RequestBody/RequestBody.module.css | 47 +++++---- .../components/RequestBody/RequestBody.tsx | 28 ++---- .../SandboxOutput/SandboxOutput.module.css | 2 +- .../SandboxOutput/SandboxOutput.tsx | 95 +++++++------------ src/lib/operations/hooks.ts | 62 ++++++++++++ src/lib/operations/server.ts | 19 ++++ src/lib/operations/util.ts | 23 ++++- src/lib/swagger/parse.ts | 2 + 21 files changed, 395 insertions(+), 149 deletions(-) create mode 100644 src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.module.css create mode 100644 src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.tsx create mode 100644 src/components/ApiSandbox/SandboxInput/components/AuthInput/index.ts create mode 100644 src/lib/operations/hooks.ts create mode 100644 src/lib/operations/server.ts diff --git a/src/components/ApiSandbox/ApiSandboxDialog.tsx b/src/components/ApiSandbox/ApiSandboxDialog.tsx index 75c2a8af..6e4693b1 100644 --- a/src/components/ApiSandbox/ApiSandboxDialog.tsx +++ b/src/components/ApiSandbox/ApiSandboxDialog.tsx @@ -25,7 +25,7 @@ export const ApiSandboxDialog = ({ operation, operations }: ApiSandboxDialogProp if (open) { setCurrentOperation(operation); } - }, [open]); + }, [open, operation]); return ( diff --git a/src/components/ApiSandbox/Sandbox.tsx b/src/components/ApiSandbox/Sandbox.tsx index 9f066d08..9b2d744c 100644 --- a/src/components/ApiSandbox/Sandbox.tsx +++ b/src/components/ApiSandbox/Sandbox.tsx @@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react'; -import { getParametersByParam } from '@/lib/operations/util'; +import { getHeaderOptions, getParametersByParam } from '@/lib/operations/util'; import { ApiOperation } from '@/lib/swagger/types'; import SandboxInput from './SandboxInput'; @@ -24,10 +24,20 @@ export const Sandbox = ({ currentOperation, operations, onChangeOperation }: San [currentOperation], ); const bodyParameters = currentOperation.requestBody; + const headers = useMemo(() => getHeaderOptions(currentOperation), [currentOperation]); const [pathParamState, setPathParamState] = useState>({}); + const [headersState, setHeadersState] = useState<{ + current: 'apikey' | 'basic'; + apikey: string; + basic: string; + }>({ + current: headers[0], + apikey: '', + basic: '', + }); - const paramState = useMemo(() => ({ path: pathParamState }), [[pathParamState]]); + const paramState = useMemo(() => ({ path: pathParamState }), [pathParamState]); const updatePathParam = (name: string, value: string) => { setPathParamState((v) => ({ ...v, [name]: value })); @@ -37,6 +47,12 @@ export const Sandbox = ({ currentOperation, operations, onChangeOperation }: San setPathParamState(Object.fromEntries(pathsParameters.map((p) => [p.name, '']))); }, [pathsParameters]); + useEffect(() => { + if (headers.length > 0 && !headers.includes(headersState.current)) { + setHeadersState((s) => ({ ...s, current: headers[0] })); + } + }, [headers, headersState]); + return ( <> setHeadersState((s) => ({ ...s, current: h }))} + onChangeHeader={(header, value) => setHeadersState((s) => ({ ...s, [header]: value }))} onChangeOperation={onChangeOperation} onUpdateState={(type, name, value) => { if (type === 'param') updatePathParam(name, value); }} /> - + ); }; diff --git a/src/components/ApiSandbox/SandboxInput/SandboxInput.module.css b/src/components/ApiSandbox/SandboxInput/SandboxInput.module.css index 3706c7dd..65e90be5 100644 --- a/src/components/ApiSandbox/SandboxInput/SandboxInput.module.css +++ b/src/components/ApiSandbox/SandboxInput/SandboxInput.module.css @@ -1,13 +1,13 @@ .root { min-width: 400px; padding: var(--space-m); - max-height: calc(100vh - var(--space-l) * 2); + height: calc(100vh - var(--space-l) * 2); overflow-y: auto; } @media (--phablet-up) { .root { - max-height: calc(100vh - var(--space-l) * 2); + height: calc(100vh - var(--space-l) * 2); } } diff --git a/src/components/ApiSandbox/SandboxInput/SandboxInput.tsx b/src/components/ApiSandbox/SandboxInput/SandboxInput.tsx index 114dceb9..c8a2700e 100644 --- a/src/components/ApiSandbox/SandboxInput/SandboxInput.tsx +++ b/src/components/ApiSandbox/SandboxInput/SandboxInput.tsx @@ -6,6 +6,7 @@ import { Tag } from '@/components/Tag'; import { operationUrl } from '@/lib/operations/util'; import { ApiOperation, ParameterObject, RequestBodyObject } from '@/lib/swagger/types'; +import AuthInput from './components/AuthInput'; import OperationSelect from './components/OperationSelect'; import PathParams from './components/PathParams'; import QueryParams from './components/QueryParams'; @@ -23,6 +24,11 @@ type SandboxInputProps = { paramState: { path: Record; }; + currentHeader: 'apikey' | 'basic'; + headers: ('apikey' | 'basic')[]; + headersState: Record; + onUpdateCurrentHeader: (h: 'apikey' | 'basic') => void; + onChangeHeader: (h: 'apikey' | 'basic', value: string) => void; onChangeOperation: (o: ApiOperation) => void; onUpdateState: (type: 'param' | 'query' | 'body', name: string, value: string) => void; }; @@ -30,10 +36,15 @@ type SandboxInputProps = { export const SandboxInput = ({ operation, operations, - onChangeOperation, parameters, paramState, + headers, + currentHeader, + headersState, + onChangeHeader, + onChangeOperation, onUpdateState, + onUpdateCurrentHeader, }: SandboxInputProps) => { const { path, query, body } = parameters; @@ -48,7 +59,15 @@ export const SandboxInput = ({ {url} -
+ + + {path.length > 0 ? ( 0 ? : null} {body ? : null} -
+
); }; diff --git a/src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.module.css b/src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.module.css new file mode 100644 index 00000000..b3e005aa --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.module.css @@ -0,0 +1,51 @@ +.root { + position: relative; + + width: 100%; +} + +.select { + border: 1px solid var(--color-border); + width: fit-content; + padding: var(--space-xs); + border-radius: var(--border-radius-m) 0 0 var(--border-radius-m); + flex-shrink: 0; +} + +.input { + padding: var(--space-xs); + width: 100%; + border-top: 1px solid var(--color-border); + border-left: 0; + border-right: 1px solid var(--color-border); + border-bottom: 1px solid var(--color-border); + border-radius: 0 var(--border-radius-m) var(--border-radius-m) 0; +} + +.selectContainer { + background-color: var(--color-background-default); + border-radius: var(--border-radius-m); + box-shadow: 0 0 2px 1px var(--color-dialog-box-shadow); + z-index: var(--layer-select); + border: 1px solid var(--color-border); + animation: contentShow 200ms ease-in-out; +} + +.selectItem { + position: relative; + display: flex; + cursor: pointer; + scroll-margin-top: var(--space-4xs); + scroll-margin-bottom: var(--space-4xs); + align-items: center; + padding: var(--space-xs) var(--space-l) var(--space-xs) var(--space-l); + outline: 2px solid transparent; + outline-offset: 2px; +} + +.selectItemIndicator { + position: absolute; + left: var(--space-3xs); + top: var(--space-s); + color: var(--color-accent-default); +} diff --git a/src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.tsx b/src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.tsx new file mode 100644 index 00000000..697ef434 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.tsx @@ -0,0 +1,66 @@ +import * as RadixSelect from '@radix-ui/react-select'; + +import { Flex } from '@/components/Flex'; +import { Icon } from '@/icons'; + +import styles from './AuthInput.module.css'; + +type AuthInputProps = { + currentHeader: 'apikey' | 'basic'; + headers: ('apikey' | 'basic')[]; + headersState: string; + onChangeHeader: (h: 'apikey' | 'basic', value: string) => void; + onUpdateCurrentHeader: (h: 'apikey' | 'basic') => void; +}; + +const authLabel = (header: 'apikey' | 'basic') => (header === 'apikey' ? 'API Key' : 'Basic'); + +const AuthInput = ({ + currentHeader, + headers, + headersState, + onChangeHeader, + onUpdateCurrentHeader, +}: AuthInputProps) => { + if (headers.length === 0) return null; + + return ( + + + + + {headers.length >= 2 && } + +
{authLabel(currentHeader)}
+
+
+
+ + + + {headers.map((h) => ( + + + + + {authLabel(h)} + + ))} + + +
+ + onChangeHeader(currentHeader, e.target.value)} + /> +
+ ); +}; + +export default AuthInput; diff --git a/src/components/ApiSandbox/SandboxInput/components/AuthInput/index.ts b/src/components/ApiSandbox/SandboxInput/components/AuthInput/index.ts new file mode 100644 index 00000000..3a7995f6 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/AuthInput/index.ts @@ -0,0 +1,3 @@ +import AuthInput from './AuthInput'; + +export default AuthInput; diff --git a/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.module.css b/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.module.css index 54f28621..8bbee533 100644 --- a/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.module.css +++ b/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.module.css @@ -21,7 +21,7 @@ z-index: var(--layer-select); max-height: 500px; border: 1px solid var(--color-border); - animation: contentShow 300ms ease-in-out; + animation: contentShow 200ms ease-in-out; } .comboboxWrapper { diff --git a/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.tsx b/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.tsx index 118d20a6..c501f5ff 100644 --- a/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.tsx +++ b/src/components/ApiSandbox/SandboxInput/components/OperationSelect/OperationSelect.tsx @@ -38,7 +38,7 @@ export default function OperationSelect({ value, options, onValueChange }: Opera matches.push(selectedLanguage); } return matches; - }, [searchValue, value]); + }, [searchValue, value, options]); return ( (
{parameters.map((param) => ( - - -
{param.name}
+ + +
{param.name}
{param.schema?.type}
{param.required ? 'required' : 'optional'} diff --git a/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.module.css b/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.module.css index 20c8ea80..17e92e19 100644 --- a/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.module.css +++ b/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.module.css @@ -1,39 +1,44 @@ -.grid { - margin-block-end: var(--column-block-padding); +.root { 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); +.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); } -.subItem { +.heading { 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); - } + padding-block: calc(var(--column-block-padding) / 2); } .param { - padding: var(--space-xs) var(--space-m); + border-top: 1px solid var(--color-border); + padding: var(--space-xs) var(--space-xs) 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); +.name { + max-width: 50%; + font-size: var(--text-body-xs); +} + +.nameName { + font-size: var(--text-body-m); +} + +.input { + width: 50%; + border: 1px solid var(--color-border); + border-radius: var(--border-radius-s); + padding: var(--space-4xs) var(--space-2xs); } -.subItemType { +.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/QueryParams/QueryParams.tsx b/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.tsx index 4b8e1ca6..3384b202 100644 --- a/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.tsx +++ b/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.tsx @@ -7,20 +7,21 @@ import { ApiOperation } from '@/lib/swagger/types'; import styles from './QueryParams.module.css'; const QueryParams = ({ parameters }: { parameters: NonNullable }) => ( -
-
-
Query params
+
+
+
Query params
+ {parameters.map((param) => ( -
- -
{param.name}
-
{param.schema?.type}
+ + +
{param.name}
+
{param.schema?.type}
- {param.required ? 'required' : 'optional'} + {param.required ? 'required' : 'optional'}
-
+ ))}
); diff --git a/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.module.css b/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.module.css index 20c8ea80..17e92e19 100644 --- a/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.module.css +++ b/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.module.css @@ -1,39 +1,44 @@ -.grid { - margin-block-end: var(--column-block-padding); +.root { 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); +.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); } -.subItem { +.heading { 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); - } + padding-block: calc(var(--column-block-padding) / 2); } .param { - padding: var(--space-xs) var(--space-m); + border-top: 1px solid var(--color-border); + padding: var(--space-xs) var(--space-xs) 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); +.name { + max-width: 50%; + font-size: var(--text-body-xs); +} + +.nameName { + font-size: var(--text-body-m); +} + +.input { + width: 50%; + border: 1px solid var(--color-border); + border-radius: var(--border-radius-s); + padding: var(--space-4xs) var(--space-2xs); } -.subItemType { +.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/RequestBody/RequestBody.tsx b/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.tsx index 32139331..75733989 100644 --- a/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.tsx +++ b/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.tsx @@ -7,18 +7,10 @@ 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'} - - )} -
+
+
+
Body params
- {requestBody.content && Object.entries(requestBody.content) .map((entry) => { @@ -36,17 +28,17 @@ export const RequestBody = ({ requestBody }: { requestBody: NonNullable { const [name, param] = p as unknown as [string, SchemaObject]; return ( -
- -
{name}
-
{param?.type}
+ + +
{name}
+
{param?.type}
- - {param?.required ? 'required' : 'optional'} + + {param.required ? 'required' : 'optional'}
-
+ ); })}
diff --git a/src/components/ApiSandbox/SandboxOutput/SandboxOutput.module.css b/src/components/ApiSandbox/SandboxOutput/SandboxOutput.module.css index 80cc4c17..eef457bc 100644 --- a/src/components/ApiSandbox/SandboxOutput/SandboxOutput.module.css +++ b/src/components/ApiSandbox/SandboxOutput/SandboxOutput.module.css @@ -38,6 +38,6 @@ .request :global(pre), .response :global(pre) { - max-height: calc((100vh - var(--space-l) * 2) * 0.35); + max-height: calc((100vh - var(--space-l) * 2) * 0.45); overflow-y: auto; } diff --git a/src/components/ApiSandbox/SandboxOutput/SandboxOutput.tsx b/src/components/ApiSandbox/SandboxOutput/SandboxOutput.tsx index 46bad26a..913095a4 100644 --- a/src/components/ApiSandbox/SandboxOutput/SandboxOutput.tsx +++ b/src/components/ApiSandbox/SandboxOutput/SandboxOutput.tsx @@ -1,80 +1,55 @@ +import { useEffect } from 'react'; + 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 { useApi } from '@/lib/operations/hooks'; +import { curlCommand, operationKey } 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; }; + header: 'apikey' | 'basic' | null; + headerValue: string | null; }; -export const SandboxOutput = ({ operation, paramState }: SandboxOutputProps) => { - const command = curlCommand(operation, paramState); +export const SandboxOutput = ({ operation, paramState, header, headerValue }: SandboxOutputProps) => { + const command = curlCommand(operation, paramState, [header, headerValue]); + + const { response, isFetching, call, reset } = useApi(operation, paramState, header, headerValue); - const stringResponse = JSON.stringify(response, null, 4); + const key = operationKey(operation); + useEffect(() => { + reset(); + }, [key, reset]); + + const statusTag = + (response?.status ?? -1) > 0 ? ( + + ) : null; + const stringResponse = + (response?.status ?? -1) > 0 && response?.body ? JSON.stringify(response.body, null, 4) : null; return ( cURL Request - @@ -82,13 +57,11 @@ export const SandboxOutput = ({ operation, paramState }: SandboxOutputProps) => {command} - 200} - className={styles.response}> - {stringResponse} - + {stringResponse ? ( + + {stringResponse} + + ) : null} ); }; diff --git a/src/lib/operations/hooks.ts b/src/lib/operations/hooks.ts new file mode 100644 index 00000000..981a828c --- /dev/null +++ b/src/lib/operations/hooks.ts @@ -0,0 +1,62 @@ +import { useCallback, useState } from 'react'; + +import { operationUrl } from '@/lib/operations/util'; +import { ApiOperation } from '@/lib/swagger/types'; + +import { callApi } from './server'; + +export const useApi = ( + op: ApiOperation, + parameters: { + path: Record; + }, + header: 'apikey' | 'basic' | null, + headerValue: string | null, +) => { + const [response, setResponse] = useState<{ status: number; body: object } | undefined>(undefined); + const [isFetching, setIsFetching] = useState(false); + + const call = () => { + setIsFetching(true); + setResponse(undefined); + + const baseUrl = operationUrl(op); + + const pathReplacedUrl = Object.entries(parameters.path).reduce((url, current) => { + const [param, value] = current; + const template = `{${param}}`; + if (value !== '' && url.includes(template)) { + return url.replaceAll(`{${param}}`, value); + } + return url; + }, baseUrl); + + const cleanedUrl = pathReplacedUrl.replaceAll('\{', '').replaceAll('\}', ''); + + const headers: HeadersInit = { + accept: 'application/json', + 'content-type': 'application/json', + }; + + if (header && headerValue) { + const headerKey = header === 'apikey' ? 'X-Api-Key' : 'authorization'; + const value = header === 'apikey' ? headerValue : `Basic ${btoa(headerValue)}`; + headers[headerKey] = value; + } + + callApi(cleanedUrl, headers) + .then((response) => { + setResponse(response); + }) + .catch((response) => { + setResponse(response); + }) + .finally(() => setIsFetching(false)); + }; + + const reset = useCallback(() => { + setResponse(undefined); + }, [setResponse]); + + return { response, isFetching, call, reset }; +}; diff --git a/src/lib/operations/server.ts b/src/lib/operations/server.ts new file mode 100644 index 00000000..a5e25441 --- /dev/null +++ b/src/lib/operations/server.ts @@ -0,0 +1,19 @@ +'use server'; + +export const callApi = async (url: string, headers: HeadersInit) => { + try { + const response = await fetch(url, { + headers, + }); + + return { + status: response.status, + body: await response.json(), + }; + } catch { + return { + status: -1, + body: { error: 'Something went wrong' }, + }; + } +}; diff --git a/src/lib/operations/util.ts b/src/lib/operations/util.ts index c52e6c55..da4a7235 100644 --- a/src/lib/operations/util.ts +++ b/src/lib/operations/util.ts @@ -1,7 +1,7 @@ import { ApiOperation } from '../swagger/types'; export const operationUrl = (operation: ApiOperation) => - `${process.env.NEXT_PUBLIC_CLOUDSMITH_API_URL}/${operation.version}${operation.path}`; + `${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 @@ -13,6 +13,15 @@ export const operationPath = (slug: string): string => { export const getParametersByParam = (operation: ApiOperation, param: string) => operation.parameters?.filter((p) => p.in === param); +export const getHeaderOptions = (operation: ApiOperation) => + Array.from( + new Set( + (operation.security ?? []) + .flatMap((s) => Object.keys(s)) + .filter((s) => s === 'apikey' || s === 'basic'), + ), + ).toSorted(); + export const operationKey = (op: ApiOperation) => `${op.method}-${op.path}`; export const curlCommand = ( @@ -20,6 +29,7 @@ export const curlCommand = ( parameters: { path: Record; }, + _header: ['apikey' | 'basic' | null, string | null], ) => { let command = `curl --request ${op.method.toUpperCase()} \\\n`; @@ -38,7 +48,16 @@ export const curlCommand = ( command += ` --url '${cleanedUrl}' \\\n`; - command += ` --header 'accept:application/json'`; + const [header, headerValue] = _header; + + if (header && headerValue) { + const headerStart = header === 'apikey' ? 'X-Api-Key: ' : 'authorization: Basic '; + const headerEnd = header === 'apikey' ? headerValue : btoa(headerValue); + command += ` --header '${headerStart}${headerEnd}' \\\n`; + } + + command += ` --header 'accept:application/json' \\\n`; + command += ` --header 'content-type: application/json' `; return command; }; diff --git a/src/lib/swagger/parse.ts b/src/lib/swagger/parse.ts index 661ceba9..cfcd6c97 100644 --- a/src/lib/swagger/parse.ts +++ b/src/lib/swagger/parse.ts @@ -93,6 +93,7 @@ export const toOperations = (schemas: { schema: OpenAPIV3.Document; version: str }; for (const { schema, version } of schemas) { + const defaultSecurity = schema.security; for (const path in schema.paths) { const pathObject = schema.paths[path]; for (const field in pathObject) { @@ -145,6 +146,7 @@ export const toOperations = (schemas: { schema: OpenAPIV3.Document; version: str slug, title: createTitle(menuSegments), experimental: operation.tags?.includes('experimental') === true, + security: operation.security ?? defaultSecurity, }); } } From 0ff6a572238d18abb972eb3779cdca732f597e4b Mon Sep 17 00:00:00 2001 From: Fernando Florenzano Date: Tue, 27 Jan 2026 21:25:14 -0300 Subject: [PATCH 10/11] path and query params --- package-lock.json | 471 +++++++++++++++++- package.json | 3 +- src/app/(api)/api/[...slug]/page.tsx | 18 + src/components/ApiSandbox/Sandbox.tsx | 17 +- .../ApiSandbox/SandboxInput/SandboxInput.tsx | 9 +- .../components/AuthInput/AuthInput.module.css | 26 +- .../components/AuthInput/AuthInput.tsx | 33 ++ .../ParamSet.module.css} | 36 +- .../components/ParamSet/ParamSet.tsx | 87 ++++ .../SandboxInput/components/ParamSet/index.ts | 5 + .../ParamSet/inputs/ParamInput.module.css | 62 +++ .../components/ParamSet/inputs/ParamInput.tsx | 90 ++++ .../components/ParamSet/inputs/index.ts | 3 + .../components/PathParams/PathParams.tsx | 60 +-- .../QueryParams/QueryParams.module.css | 45 -- .../components/QueryParams/QueryParams.tsx | 59 ++- .../RequestBody/RequestBody.module.css | 19 - .../components/RequestBody/RequestBody.tsx | 86 ++-- .../SandboxOutput/SandboxOutput.tsx | 1 + src/icons/icon-registry.tsx | 56 +-- src/icons/svgs/Question.tsx | 18 + src/icons/svgs/action/Eye.tsx | 20 + src/icons/svgs/action/EyeSlashed.tsx | 12 + src/icons/svgs/action/eyeslashed.svg | 3 + src/lib/operations/hooks.ts | 19 +- src/lib/operations/util.ts | 17 +- src/lib/swagger/types.ts | 2 +- 27 files changed, 1052 insertions(+), 225 deletions(-) rename src/components/ApiSandbox/SandboxInput/components/{PathParams/PathParams.module.css => ParamSet/ParamSet.module.css} (63%) create mode 100644 src/components/ApiSandbox/SandboxInput/components/ParamSet/ParamSet.tsx create mode 100644 src/components/ApiSandbox/SandboxInput/components/ParamSet/index.ts create mode 100644 src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/ParamInput.module.css create mode 100644 src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/ParamInput.tsx create mode 100644 src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/index.ts delete mode 100644 src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.module.css create mode 100644 src/icons/svgs/Question.tsx create mode 100644 src/icons/svgs/action/Eye.tsx create mode 100644 src/icons/svgs/action/EyeSlashed.tsx create mode 100644 src/icons/svgs/action/eyeslashed.svg diff --git a/package-lock.json b/package-lock.json index ee939ebf..1150054f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", "@next/mdx": "15.1.6", + "@radix-ui/react-tooltip": "^1.2.8", "@vercel/analytics": "^1.6.1", "class-variance-authority": "^0.7.1", "date-fns": "^4.1.0", @@ -3864,7 +3865,6 @@ "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" @@ -3874,7 +3874,6 @@ "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", @@ -3885,7 +3884,6 @@ "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" @@ -3899,7 +3897,6 @@ "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": { @@ -6174,6 +6171,374 @@ } } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", @@ -6209,6 +6574,39 @@ } } }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-escape-keydown": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", @@ -6299,13 +6697,50 @@ } }, "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz", - "integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==", - "dev": true, + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", "license": "MIT", "dependencies": { - "@radix-ui/react-primitive": "2.0.2" + "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", @@ -6322,6 +6757,24 @@ } } }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/rect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", @@ -7123,7 +7576,7 @@ "version": "19.0.3", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.3.tgz", "integrity": "sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.0.0" diff --git a/package.json b/package.json index 71dc3aa4..09b52cd6 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@mdx-js/loader": "^3.1.0", "@mdx-js/react": "^3.1.0", "@next/mdx": "15.1.6", + "@radix-ui/react-tooltip": "^1.2.8", "@vercel/analytics": "^1.6.1", "class-variance-authority": "^0.7.1", "date-fns": "^4.1.0", @@ -43,8 +44,8 @@ "@lehoczky/postcss-fluid": "^1.0.3", "@next/env": "15.1.7", "@radix-ui/react-dialog": "1.1.4", - "@radix-ui/react-tabs": "^1.1.3", "@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", "@svgr/webpack": "^8.1.0", diff --git a/src/app/(api)/api/[...slug]/page.tsx b/src/app/(api)/api/[...slug]/page.tsx index 82c7f8c5..62cbef42 100644 --- a/src/app/(api)/api/[...slug]/page.tsx +++ b/src/app/(api)/api/[...slug]/page.tsx @@ -62,6 +62,24 @@ export const generateStaticParams = async () => { // Generate swagger slugs const schemas = await parseSchemas(); const operations = toOperations(schemas); + + const queryParams = operations + .filter((op) => (op.parameters?.filter((p) => p.in === 'query') ?? []).length > 0) + .flatMap((op) => ({ + method: op.method, + path: op.path, + params: op.parameters?.filter((p) => p.in === 'query') ?? [], + })); + console.log(JSON.stringify(queryParams)); + const schemaSet = new Set(); + queryParams.forEach((q) => { + q.params.forEach((p) => { + schemaSet.add(JSON.stringify(p.schema)); + }); + }); + + console.log(JSON.stringify({ schemaSet: Array.from(schemaSet) })); + const operationSlugs = operations.map((op) => ({ slug: toRouteSegments(op.slug) })); return mdxSlugs.concat(operationSlugs); diff --git a/src/components/ApiSandbox/Sandbox.tsx b/src/components/ApiSandbox/Sandbox.tsx index 9b2d744c..6c5520bc 100644 --- a/src/components/ApiSandbox/Sandbox.tsx +++ b/src/components/ApiSandbox/Sandbox.tsx @@ -27,6 +27,8 @@ export const Sandbox = ({ currentOperation, operations, onChangeOperation }: San const headers = useMemo(() => getHeaderOptions(currentOperation), [currentOperation]); const [pathParamState, setPathParamState] = useState>({}); + const [queryParamState, setQueryParamState] = useState>({}); + const [headersState, setHeadersState] = useState<{ current: 'apikey' | 'basic'; apikey: string; @@ -37,16 +39,28 @@ export const Sandbox = ({ currentOperation, operations, onChangeOperation }: San basic: '', }); - const paramState = useMemo(() => ({ path: pathParamState }), [pathParamState]); + const paramState = useMemo( + () => ({ path: pathParamState, query: queryParamState }), + [pathParamState, queryParamState], + ); const updatePathParam = (name: string, value: string) => { setPathParamState((v) => ({ ...v, [name]: value })); }; + const updateQueryParam = (name: string, value: string) => { + setQueryParamState((v) => ({ ...v, [name]: value })); + }; useEffect(() => { setPathParamState(Object.fromEntries(pathsParameters.map((p) => [p.name, '']))); }, [pathsParameters]); + useEffect(() => { + setQueryParamState( + Object.fromEntries(queryParameters.map((p) => [p.name, `${p.schema?.default ?? ''}`])), + ); + }, [queryParameters]); + useEffect(() => { if (headers.length > 0 && !headers.includes(headersState.current)) { setHeadersState((s) => ({ ...s, current: headers[0] })); @@ -72,6 +86,7 @@ export const Sandbox = ({ currentOperation, operations, onChangeOperation }: San onChangeOperation={onChangeOperation} onUpdateState={(type, name, value) => { if (type === 'param') updatePathParam(name, value); + if (type === 'query') updateQueryParam(name, value); }} /> ; + query: Record; }; currentHeader: 'apikey' | 'basic'; headers: ('apikey' | 'basic')[]; @@ -76,7 +77,13 @@ export const SandboxInput = ({ /> ) : null} - {query.length > 0 ? : null} + {query.length > 0 ? ( + onUpdateState('query', name, value)} + /> + ) : null} {body ? : null} diff --git a/src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.module.css b/src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.module.css index b3e005aa..567a6fe7 100644 --- a/src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.module.css +++ b/src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.module.css @@ -1,6 +1,5 @@ .root { position: relative; - width: 100%; } @@ -43,9 +42,34 @@ outline-offset: 2px; } +.selectItem:hover { + background-color: var(--color-background-info); +} + .selectItemIndicator { position: absolute; left: var(--space-3xs); top: var(--space-s); color: var(--color-accent-default); } + +.iconsContainer { + position: absolute; + top: var(--space-s); + right: var(--space-s); + color: var(--color-text-tertiary); +} + +.hideButton { + cursor: pointer; +} + +.tooltip { + background: var(--color-background-default); + padding: var(--space-2xs) var(--space-xs); + color: var(--color-text-primary); + border-radius: var(--border-radius-m); + box-shadow: 0 0 2px 1px var(--color-dialog-box-shadow); + z-index: var(--layer-select); + border: 1px solid var(--color-border); +} diff --git a/src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.tsx b/src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.tsx index 697ef434..26689e87 100644 --- a/src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.tsx +++ b/src/components/ApiSandbox/SandboxInput/components/AuthInput/AuthInput.tsx @@ -1,4 +1,7 @@ +import { useState } from 'react'; + import * as RadixSelect from '@radix-ui/react-select'; +import * as RadixTooltip from '@radix-ui/react-tooltip'; import { Flex } from '@/components/Flex'; import { Icon } from '@/icons'; @@ -22,6 +25,8 @@ const AuthInput = ({ onChangeHeader, onUpdateCurrentHeader, }: AuthInputProps) => { + const [hideAuth, setHideAuth] = useState(false); + if (headers.length === 0) return null; return ( @@ -55,10 +60,38 @@ const AuthInput = ({ onChangeHeader(currentHeader, e.target.value)} /> + + + + + + + + + {hideAuth ? 'Show' : 'Hide'} + + + + + + + + + + + Header + + + + ); }; diff --git a/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.module.css b/src/components/ApiSandbox/SandboxInput/components/ParamSet/ParamSet.module.css similarity index 63% rename from src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.module.css rename to src/components/ApiSandbox/SandboxInput/components/ParamSet/ParamSet.module.css index 17e92e19..c2975448 100644 --- a/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.module.css +++ b/src/components/ApiSandbox/SandboxInput/components/ParamSet/ParamSet.module.css @@ -17,11 +17,38 @@ padding-block: calc(var(--column-block-padding) / 2); } -.param { +.paramWrapper { + width: 100%; +} + +.param, +.toggle { border-top: 1px solid var(--color-border); + padding: var(--space-xs) var(--space-xs) 0 var(--space-m); +} + +.param:not(:has(+ .secondRow)), +.toggle { padding: var(--space-xs) var(--space-xs) var(--space-xs) var(--space-m); } +.toggle { + --svg-path-fill: var(--color-text-tertiary); + + cursor: pointer; + color: var(--color-text-tertiary); + font-size: var(--text-body-s); +} + +.secondRow { + padding: 0 var(--space-xs) var(--space-xs) var(--space-m); +} + +.description { + font-size: var(--text-body-xs); + color: var(--color-text-tertiary); +} + .name { max-width: 50%; font-size: var(--text-body-xs); @@ -31,13 +58,6 @@ font-size: var(--text-body-m); } -.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); diff --git a/src/components/ApiSandbox/SandboxInput/components/ParamSet/ParamSet.tsx b/src/components/ApiSandbox/SandboxInput/components/ParamSet/ParamSet.tsx new file mode 100644 index 00000000..d6550732 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/ParamSet/ParamSet.tsx @@ -0,0 +1,87 @@ +import { ReactNode } from 'react'; + +import { cx } from 'class-variance-authority'; + +import { Flex } from '@/components/Flex'; +import { Paragraph } from '@/components/Paragraph'; +import { Tag } from '@/components/Tag'; +import { Icon } from '@/icons'; +import { NonArraySchemaObjectType } from '@/lib/swagger/types'; + +import ParamInput from './inputs'; +import styles from './ParamSet.module.css'; + +type ParamSetProps = { + heading: string; + children: ReactNode; +}; + +const ParamSet = ({ heading, children }: ParamSetProps) => { + return ( +
+
+
{heading}
+
+ + {children} +
+ ); +}; + +type ParamProps = { + name: string; + description?: string; + type: NonArraySchemaObjectType; + required?: boolean; + value?: string; + onValueChange: (v: string) => void; +}; + +export const Param = ({ + name, + description, + type, + required = false, + value = '', + onValueChange, +}: ParamProps) => ( + + + +
{name}
+
{type}
+
+ {required ? 'required' : 'optional'} +
+
+ + onValueChange(v)} /> +
+ {description && ( + + {description} + + )} +
+); + +type ParamToggleProps = { + paramTag: string; + show: boolean; + onChangeShow: (s: boolean) => void; +}; + +export const ParamToggle = ({ paramTag, show, onChangeShow }: ParamToggleProps) => ( + + + +); + +export default ParamSet; diff --git a/src/components/ApiSandbox/SandboxInput/components/ParamSet/index.ts b/src/components/ApiSandbox/SandboxInput/components/ParamSet/index.ts new file mode 100644 index 00000000..485d91cf --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/ParamSet/index.ts @@ -0,0 +1,5 @@ +import ParamSet, { Param } from './ParamSet'; + +export { Param }; + +export default ParamSet; diff --git a/src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/ParamInput.module.css b/src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/ParamInput.module.css new file mode 100644 index 00000000..4d1fc62f --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/ParamInput.module.css @@ -0,0 +1,62 @@ +.root { + position: relative; + flex-shrink: 0; + width: 50%; + border: 1px solid var(--color-border); + border-radius: var(--border-radius-s); + padding: var(--space-4xs) var(--space-2xs); +} + +.select { + cursor: pointer; +} + +.reset { + position: absolute; + top: var(--space-2xs); + right: var(--space-xs); + cursor: pointer; +} + +.selectItem span, +.selectValue { + text-transform: capitalize; +} + +.selectContainer { + background-color: var(--color-background-default); + border-radius: var(--border-radius-m); + box-shadow: 0 0 2px 1px var(--color-dialog-box-shadow); + z-index: var(--layer-select); + border: 1px solid var(--color-border); + animation: contentShow 200ms ease-in-out; +} + +.selectItem { + position: relative; + display: flex; + cursor: pointer; + scroll-margin-top: var(--space-4xs); + scroll-margin-bottom: var(--space-4xs); + align-items: center; + padding: var(--space-xs) var(--space-l) var(--space-xs) var(--space-l); + outline: 2px solid transparent; + outline-offset: 2px; +} + +.selectItem:hover { + background-color: var(--color-background-info); +} + +.selectItemIndicator { + position: absolute; + left: var(--space-3xs); + top: var(--space-s); + color: var(--color-accent-default); +} + +.icon { + --svg-path-fill: var(--color-text-tertiary); + + color: var(--color-text-tertiary); +} diff --git a/src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/ParamInput.tsx b/src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/ParamInput.tsx new file mode 100644 index 00000000..974b3358 --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/ParamInput.tsx @@ -0,0 +1,90 @@ +import { useCallback, useEffect, useState } from 'react'; + +import * as RadixSelect from '@radix-ui/react-select'; + +import { Flex } from '@/components/Flex'; +import { Icon } from '@/icons'; +import { NonArraySchemaObjectType } from '@/lib/swagger/types'; +import { debounce } from '@/lib/util'; + +import styles from './ParamInput.module.css'; + +type ParamInput = { + type: NonArraySchemaObjectType; + value: string; + onChange: (v: string) => void; +}; + +const ParamInput = ({ type, value: _value, onChange: _onChange }: ParamInput) => { + const [value, setValue] = useState(_value); + useEffect(() => setValue(_value), [_value]); + + const onChange = useCallback(debounce(_onChange, 200), [_onChange]); + + if (type === 'boolean') { + return ( +
+ + + + + + + {value} + + + + + + + {['true', 'false'].map((b) => ( + + + + + {b} + + ))} + + + + {value !== '' && ( + + )} +
+ ); + } + + if (type === 'integer') + return ( + { + setValue(e.target.value); + onChange(e.target.value); + }} + /> + ); + + return ( + { + setValue(e.target.value); + onChange(e.target.value); + }} + /> + ); +}; + +export default ParamInput; diff --git a/src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/index.ts b/src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/index.ts new file mode 100644 index 00000000..e38faedf --- /dev/null +++ b/src/components/ApiSandbox/SandboxInput/components/ParamSet/inputs/index.ts @@ -0,0 +1,3 @@ +import ParamInput from './ParamInput'; + +export default ParamInput; diff --git a/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.tsx b/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.tsx index e95beed0..bdc38aed 100644 --- a/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.tsx +++ b/src/components/ApiSandbox/SandboxInput/components/PathParams/PathParams.tsx @@ -1,13 +1,6 @@ -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'; +import ParamSet, { Param } from '../ParamSet'; type PathParamsProps = { parameters: NonNullable; @@ -15,50 +8,19 @@ type PathParamsProps = { 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)} /> -
+ onUpdateParam(param.name, v)} + /> ))} -
+ ); 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 deleted file mode 100644 index 17e92e19..00000000 --- a/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.module.css +++ /dev/null @@ -1,45 +0,0 @@ -.root { - border: 1px solid var(--color-border); - border-radius: var(--border-radius-m); - width: 100%; -} - -.header { - align-content: center; - border-top: 0; - border-radius: var(--border-radius-m) var(--border-radius-m) 0 0; - background-color: var(--color-background-light); - color: var(--brand-color-grey-7); -} - -.heading { - padding-inline: var(--column-inline-padding); - padding-block: calc(var(--column-block-padding) / 2); -} - -.param { - 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-xs); -} - -.nameName { - font-size: var(--text-body-m); -} - -.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/QueryParams/QueryParams.tsx b/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.tsx index 3384b202..3d6e9842 100644 --- a/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.tsx +++ b/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.tsx @@ -1,29 +1,42 @@ -import { cx } from 'class-variance-authority'; +import { useMemo, useState } from 'react'; -import { Flex } from '@/components/Flex'; -import { Tag } from '@/components/Tag'; -import { ApiOperation } from '@/lib/swagger/types'; +import { ApiOperation, NonArraySchemaObjectType } from '@/lib/swagger/types'; -import styles from './QueryParams.module.css'; +import ParamSet, { Param } from '../ParamSet'; +import { ParamToggle } from '../ParamSet/ParamSet'; -const QueryParams = ({ parameters }: { parameters: NonNullable }) => ( -
-
-
Query params
-
+type QueryParamsProps = { + parameters: NonNullable; + state: Record; + onUpdateParam: (name: string, value: string) => void; +}; - {parameters.map((param) => ( - - -
{param.name}
-
{param.schema?.type}
-
- {param.required ? 'required' : 'optional'} -
-
-
- ))} -
-); +const QueryParams = ({ parameters, state, onUpdateParam }: QueryParamsProps) => { + const [showAll, setShowAll] = useState(false); + const optionalExists = useMemo( + () => parameters.some((p) => p.required == null || !p.required), + [parameters], + ); + const displayedParameters = useMemo(() => { + return parameters.filter((param) => showAll || param.required); + }, [parameters, showAll]); + return ( + + {displayedParameters.map((param) => ( + onUpdateParam(param.name, v)} + /> + ))} + {optionalExists && } + + ); +}; export default QueryParams; diff --git a/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.module.css b/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.module.css index 17e92e19..d1b38885 100644 --- a/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.module.css +++ b/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.module.css @@ -1,22 +1,3 @@ -.root { - border: 1px solid var(--color-border); - border-radius: var(--border-radius-m); - width: 100%; -} - -.header { - align-content: center; - border-top: 0; - border-radius: var(--border-radius-m) var(--border-radius-m) 0 0; - background-color: var(--color-background-light); - color: var(--brand-color-grey-7); -} - -.heading { - padding-inline: var(--column-inline-padding); - padding-block: calc(var(--column-block-padding) / 2); -} - .param { border-top: 1px solid var(--color-border); padding: var(--space-xs) var(--space-xs) var(--space-xs) var(--space-m); diff --git a/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.tsx b/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.tsx index 75733989..98496a52 100644 --- a/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.tsx +++ b/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.tsx @@ -1,47 +1,59 @@ -import { cx } from 'class-variance-authority'; +import { useMemo, useState } from 'react'; import { Flex } from '@/components/Flex'; import { Tag } from '@/components/Tag'; import { ApiOperation, SchemaObject } from '@/lib/swagger/types'; +import ParamSet from '../ParamSet'; +import { ParamToggle } from '../ParamSet/ParamSet'; import styles from './RequestBody.module.css'; -export const RequestBody = ({ requestBody }: { requestBody: NonNullable }) => ( -
-
-
Body params
-
- {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 const RequestBody = ({ requestBody }: { requestBody: NonNullable }) => { + const [showAll, setShowAll] = useState(false); + + const parameterEntries = 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)); + + const optionalExists = useMemo( + () => parameterEntries.some((p) => p[1].required == null || !p[1].required), + [parameterEntries], + ); + const displayedParameters = useMemo(() => { + return parameterEntries.filter((param) => showAll || param[1].required); + }, [parameterEntries, showAll]); + + return ( + + {displayedParameters.map((p) => { + const [name, param] = p as unknown as [string, SchemaObject]; + return ( + + +
{name}
+
{param?.type}
+
+ + {param.required ? 'required' : 'optional'} + +
- ); - })} -
-); + + ); + })} + {optionalExists && } + + ); +}; export default RequestBody; diff --git a/src/components/ApiSandbox/SandboxOutput/SandboxOutput.tsx b/src/components/ApiSandbox/SandboxOutput/SandboxOutput.tsx index 913095a4..3ba07b9c 100644 --- a/src/components/ApiSandbox/SandboxOutput/SandboxOutput.tsx +++ b/src/components/ApiSandbox/SandboxOutput/SandboxOutput.tsx @@ -15,6 +15,7 @@ type SandboxOutputProps = { operation: ApiOperation; paramState: { path: Record; + query: Record; }; header: 'apikey' | 'basic' | null; headerValue: string | null; diff --git a/src/icons/icon-registry.tsx b/src/icons/icon-registry.tsx index cfe4bdb8..1d27fad6 100644 --- a/src/icons/icon-registry.tsx +++ b/src/icons/icon-registry.tsx @@ -1,31 +1,20 @@ -import { createIconRegistry } from './util/create-icon-registry'; - -import { RegoIcon } from './svgs/Rego'; -import { MenuIcon } from './svgs/Menu'; -import { ChevronIcon } from './svgs/Chevron'; -import { ChevronSmallIcon } from './svgs/ChevronSmall'; -import { ArrowIcon } from './svgs/Arrow'; -import { SearchIcon } from './svgs/Search'; -import { ExternalIcon } from './svgs/External'; -import { EditIcon } from './svgs/Edit'; -import { GithubIcon } from './svgs/Github'; -import { InfoIcon } from './svgs/Info'; - -import { UtilityDocumentationIcon } from './svgs/utility/Documentation'; -import { UtilityGuideIcon } from './svgs/utility/Guide'; -import { UtilityApiIcon } from './svgs/utility/Api'; - +import { ActionApiIcon } from './svgs/action/Api'; +import { ActionCheckIcon } from './svgs/action/Check'; import { CloseIcon } from './svgs/action/Close'; +import { ActionCopyIcon } from './svgs/action/Copy'; import { ActionDocumentationIcon } from './svgs/action/Documentation'; +import { ActionErrorIcon } from './svgs/action/Error'; +import { ActionEyeIcon } from './svgs/action/Eye'; +import { ActionEyeSlashedIcon } from './svgs/action/EyeSlashed'; import { ActionGuideIcon } from './svgs/action/Guide'; -import { ActionApiIcon } from './svgs/action/Api'; import { ActionLinkIcon } from './svgs/action/Link'; import { ActionPlayIcon } from './svgs/action/Play'; -import { ActionCopyIcon } from './svgs/action/Copy'; -import { ActionCheckIcon } from './svgs/action/Check'; -import { ActionErrorIcon } from './svgs/action/Error'; +import { ArrowIcon } from './svgs/Arrow'; +import { ChevronIcon } from './svgs/Chevron'; +import { ChevronSmallIcon } from './svgs/ChevronSmall'; +import { EditIcon } from './svgs/Edit'; import { EnterIcon } from './svgs/Enter'; - +import { ExternalIcon } from './svgs/External'; import { FormatAlpineIcon } from './svgs/format/Alpine'; import { FormatCargoIcon } from './svgs/format/Cargo'; import { FormatChocolateyIcon } from './svgs/format/Chocolatey'; @@ -39,9 +28,9 @@ import { FormatDebianIcon } from './svgs/format/Debian'; import { FormatDockerIcon } from './svgs/format/Docker'; import { FormatGoIcon } from './svgs/format/Go'; import { FormatGradleIcon } from './svgs/format/Gradle'; -import { FormatHuggingFaceIcon } from './svgs/format/HuggingFace'; import { FormatHelmIcon } from './svgs/format/Helm'; import { FormatHexIcon } from './svgs/format/Hex'; +import { FormatHuggingFaceIcon } from './svgs/format/HuggingFace'; import { FormatLuarocksIcon } from './svgs/format/Luarocks'; import { FormatMavenIcon } from './svgs/format/Maven'; import { FormatNpmIcon } from './svgs/format/Npm'; @@ -57,14 +46,14 @@ import { FormatSwiftIcon } from './svgs/format/Swift'; import { FormatTerraformIcon } from './svgs/format/Terraform'; import { FormatUnityIcon } from './svgs/format/Unity'; import { FormatVagrantIcon } from './svgs/format/Vagrant'; - +import { GithubIcon } from './svgs/Github'; +import { HomepageAPIIcon } from './svgs/homepage/API'; import { HomepageDocumentationIcon } from './svgs/homepage/Documentation'; import { HomepageGuideIcon } from './svgs/homepage/Guide'; -import { HomepageAPIIcon } from './svgs/homepage/API'; - -import { IntegrationArgoCDIcon } from './svgs/integration/ArgoCD'; +import { InfoIcon } from './svgs/Info'; import { IntegrationAikidoIcon } from './svgs/integration/Aikido'; import { IntegrationAnsibleIcon } from './svgs/integration/Ansible'; +import { IntegrationArgoCDIcon } from './svgs/integration/ArgoCD'; import { IntegrationAWSCodeBuildIcon } from './svgs/integration/AWSCodeBuild'; import { IntegrationAzureDevOpsIcon } from './svgs/integration/AzureDevOps'; import { IntegrationBitbucketPipelinesIcon } from './svgs/integration/BitbucketPipelines'; @@ -94,8 +83,16 @@ import { IntegrationTeamCityIcon } from './svgs/integration/TeamCity'; import { IntegrationTerraformIcon } from './svgs/integration/Terraform'; import { IntegrationTheiaIDEIcon } from './svgs/integration/TheiaIDE'; import { IntegrationTravisCIIcon } from './svgs/integration/TravisCI'; -import { IntegrationZapierIcon } from './svgs/integration/Zapier'; import { IntegrationVSCodeIcon } from './svgs/integration/VSCode'; +import { IntegrationZapierIcon } from './svgs/integration/Zapier'; +import { MenuIcon } from './svgs/Menu'; +import { QuestionIcon } from './svgs/Question'; +import { RegoIcon } from './svgs/Rego'; +import { SearchIcon } from './svgs/Search'; +import { UtilityApiIcon } from './svgs/utility/Api'; +import { UtilityDocumentationIcon } from './svgs/utility/Documentation'; +import { UtilityGuideIcon } from './svgs/utility/Guide'; +import { createIconRegistry } from './util/create-icon-registry'; export const iconRegistry = createIconRegistry({ menu: MenuIcon, @@ -127,6 +124,7 @@ export const iconRegistry = createIconRegistry({ edit: EditIcon, github: GithubIcon, info: InfoIcon, + question: QuestionIcon, 'utility/documentation': UtilityDocumentationIcon, 'utility/guide': UtilityGuideIcon, 'utility/api': UtilityApiIcon, @@ -139,6 +137,8 @@ export const iconRegistry = createIconRegistry({ 'action/copy': ActionCopyIcon, 'action/check': ActionCheckIcon, 'action/error': ActionErrorIcon, + 'action/eye': ActionEyeIcon, + 'action/eye-slashed': ActionEyeSlashedIcon, 'format/alpine': FormatAlpineIcon, 'format/cargo': FormatCargoIcon, 'format/chocolatey': FormatChocolateyIcon, diff --git a/src/icons/svgs/Question.tsx b/src/icons/svgs/Question.tsx new file mode 100644 index 00000000..b271480d --- /dev/null +++ b/src/icons/svgs/Question.tsx @@ -0,0 +1,18 @@ +import { createIcon, SpecificIconProps } from '../util/create-icon'; + +export const QuestionIcon = createIcon( + 'question', + ({ width = 16, height = 16, ...props }) => ({ + ...props, + width, + height, + viewBox: '0 0 16 16', + fill: 'none', + children: ( + + ), + }), +); diff --git a/src/icons/svgs/action/Eye.tsx b/src/icons/svgs/action/Eye.tsx new file mode 100644 index 00000000..1ad63a8e --- /dev/null +++ b/src/icons/svgs/action/Eye.tsx @@ -0,0 +1,20 @@ +import { createIcon } from '../../util/create-icon'; + +export const ActionEyeIcon = createIcon('action/eye', (props) => ({ + ...props, + fill: 'none', + children: ( + <> + + + + ), +})); diff --git a/src/icons/svgs/action/EyeSlashed.tsx b/src/icons/svgs/action/EyeSlashed.tsx new file mode 100644 index 00000000..22b5ad3a --- /dev/null +++ b/src/icons/svgs/action/EyeSlashed.tsx @@ -0,0 +1,12 @@ +import { createIcon } from '../../util/create-icon'; + +export const ActionEyeSlashedIcon = createIcon('action/eye-slashed', (props) => ({ + ...props, + fill: 'none', + children: ( + + ), +})); diff --git a/src/icons/svgs/action/eyeslashed.svg b/src/icons/svgs/action/eyeslashed.svg new file mode 100644 index 00000000..ba9ad5ee --- /dev/null +++ b/src/icons/svgs/action/eyeslashed.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/lib/operations/hooks.ts b/src/lib/operations/hooks.ts index 981a828c..0158fcdc 100644 --- a/src/lib/operations/hooks.ts +++ b/src/lib/operations/hooks.ts @@ -9,6 +9,7 @@ export const useApi = ( op: ApiOperation, parameters: { path: Record; + query: Record; }, header: 'apikey' | 'basic' | null, headerValue: string | null, @@ -33,6 +34,22 @@ export const useApi = ( const cleanedUrl = pathReplacedUrl.replaceAll('\{', '').replaceAll('\}', ''); + const finalUrl = Object.entries(parameters.query) + .filter((entry) => entry[1] !== '') + .reduce((url, current, index) => { + let currenUrl: string = url; + if (index === 0) { + currenUrl += '?'; + } else { + currenUrl += '&'; + } + currenUrl += `${current[0]}=${current[1]}`; + + return currenUrl; + }, cleanedUrl); + + console.log({ finalUrl }); + const headers: HeadersInit = { accept: 'application/json', 'content-type': 'application/json', @@ -44,7 +61,7 @@ export const useApi = ( headers[headerKey] = value; } - callApi(cleanedUrl, headers) + callApi(finalUrl, headers) .then((response) => { setResponse(response); }) diff --git a/src/lib/operations/util.ts b/src/lib/operations/util.ts index da4a7235..032c4c45 100644 --- a/src/lib/operations/util.ts +++ b/src/lib/operations/util.ts @@ -28,6 +28,7 @@ export const curlCommand = ( op: ApiOperation, parameters: { path: Record; + query: Record; }, _header: ['apikey' | 'basic' | null, string | null], ) => { @@ -46,7 +47,21 @@ export const curlCommand = ( const cleanedUrl = pathReplacedUrl.replaceAll('\{', '').replaceAll('\}', ''); - command += ` --url '${cleanedUrl}' \\\n`; + const finalUrl = Object.entries(parameters.query) + .filter((entry) => entry[1] !== '') + .reduce((url, current, index) => { + let currenUrl: string = url; + if (index === 0) { + currenUrl += '?'; + } else { + currenUrl += '&'; + } + currenUrl += `${current[0]}=${current[1]}`; + + return currenUrl; + }, cleanedUrl); + + command += ` --url '${finalUrl}' \\\n`; const [header, headerValue] = _header; diff --git a/src/lib/swagger/types.ts b/src/lib/swagger/types.ts index c499e946..fbd019cd 100644 --- a/src/lib/swagger/types.ts +++ b/src/lib/swagger/types.ts @@ -100,7 +100,7 @@ interface ParameterBaseObject { [media: string]: MediaTypeObject; }; } -type NonArraySchemaObjectType = 'boolean' | 'object' | 'number' | 'string' | 'integer'; +export type NonArraySchemaObjectType = 'boolean' | 'object' | 'number' | 'string' | 'integer'; type ArraySchemaObjectType = 'array'; export type SchemaObject = ArraySchemaObject | NonArraySchemaObject; interface ArraySchemaObject extends BaseSchemaObject { From 010b0b0f8458b75d9e507659f62d8dcc6c988b63 Mon Sep 17 00:00:00 2001 From: Fernando Florenzano Date: Tue, 27 Jan 2026 21:33:21 -0300 Subject: [PATCH 11/11] reset show all params and remove query analysis --- src/app/(api)/api/[...slug]/page.tsx | 17 --------- .../components/QueryParams/QueryParams.tsx | 5 ++- .../components/RequestBody/RequestBody.tsx | 36 +++++++++++-------- 3 files changed, 26 insertions(+), 32 deletions(-) diff --git a/src/app/(api)/api/[...slug]/page.tsx b/src/app/(api)/api/[...slug]/page.tsx index 62cbef42..66776885 100644 --- a/src/app/(api)/api/[...slug]/page.tsx +++ b/src/app/(api)/api/[...slug]/page.tsx @@ -63,23 +63,6 @@ export const generateStaticParams = async () => { const schemas = await parseSchemas(); const operations = toOperations(schemas); - const queryParams = operations - .filter((op) => (op.parameters?.filter((p) => p.in === 'query') ?? []).length > 0) - .flatMap((op) => ({ - method: op.method, - path: op.path, - params: op.parameters?.filter((p) => p.in === 'query') ?? [], - })); - console.log(JSON.stringify(queryParams)); - const schemaSet = new Set(); - queryParams.forEach((q) => { - q.params.forEach((p) => { - schemaSet.add(JSON.stringify(p.schema)); - }); - }); - - console.log(JSON.stringify({ schemaSet: Array.from(schemaSet) })); - const operationSlugs = operations.map((op) => ({ slug: toRouteSegments(op.slug) })); return mdxSlugs.concat(operationSlugs); diff --git a/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.tsx b/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.tsx index 3d6e9842..211a0fb1 100644 --- a/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.tsx +++ b/src/components/ApiSandbox/SandboxInput/components/QueryParams/QueryParams.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { ApiOperation, NonArraySchemaObjectType } from '@/lib/swagger/types'; @@ -13,6 +13,9 @@ type QueryParamsProps = { const QueryParams = ({ parameters, state, onUpdateParam }: QueryParamsProps) => { const [showAll, setShowAll] = useState(false); + useEffect(() => { + setShowAll(false); + }, [parameters]); const optionalExists = useMemo( () => parameters.some((p) => p.required == null || !p.required), [parameters], diff --git a/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.tsx b/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.tsx index 98496a52..d2aa440e 100644 --- a/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.tsx +++ b/src/components/ApiSandbox/SandboxInput/components/RequestBody/RequestBody.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { Flex } from '@/components/Flex'; import { Tag } from '@/components/Tag'; @@ -11,19 +11,27 @@ import styles from './RequestBody.module.css'; export const RequestBody = ({ requestBody }: { requestBody: NonNullable }) => { const [showAll, setShowAll] = useState(false); - const parameterEntries = 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)); + const parameterEntries = useMemo( + () => + 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)), + [requestBody], + ); + + useEffect(() => { + setShowAll(false); + }, [parameterEntries]); const optionalExists = useMemo( () => parameterEntries.some((p) => p[1].required == null || !p[1].required),