From 026d53418c887f40f3f0c5bbc44c3c90bd19e173 Mon Sep 17 00:00:00 2001 From: Fernando Florenzano Date: Thu, 15 Jan 2026 12:51:21 -0300 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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}`;