export declare const apiPlaygroundTemplate = "\"use client\";\nimport React, { useCallback, useMemo, useRef, useState } from \"react\";\nimport styled, { css } from \"styled-components\";\nimport {\n styledSmall,\n interactiveStyles,\n Input,\n Textarea,\n Modal,\n} from \"cherry-styled-components\";\nimport { mq, Theme } from \"@/app/theme\";\nimport { Code, CodeTabs } from \"@/components/layout/Code\";\nimport { Icon } from \"@/components/layout/Icon\";\nimport { Accordion } from \"@/components/layout/Accordion\";\nimport { Badge, httpMethodBadgeColor } from \"@/components/layout/Badge\";\nimport { Callout } from \"@/components/layout/Callout\";\nimport { CopyButton } from \"@/components/layout/CopyButton\";\nimport { Spinner } from \"@/components/Spinner\";\nimport { matchAllowlist, matchAllowlistIn } from \"@/utils/playgroundAllowlist\";\nimport type { AllowlistEntry } from \"@/utils/playgroundAllowlist\";\nimport { buildSnippets } from \"@/utils/apiSnippets\";\nimport type { SnippetLanguage } from \"@/utils/apiSnippets\";\nimport {\n LOCATIONS,\n LOCATION_TITLES,\n applyPathParams,\n bufferToBase64,\n classifyResponse,\n codeLanguage,\n firstJsonExample,\n prettyBody,\n schemaTypeLabel,\n serverBase,\n} from \"@/utils/apiPlayground\";\nimport type { BuiltRequest, PlaygroundResponse } from \"@/utils/apiPlayground\";\nimport type { OperationDescriptor, ParameterDescriptor } from \"@/types/openapi\";\n\nexport interface ApiPlaygroundProps {\n operation: OperationDescriptor;\n defaultMode?: \"proxy\" | \"direct\";\n allowDirect?: boolean;\n // Extra client-side allowlist entries. The server proxy always enforces the\n // build-time allowlist regardless; this only relaxes the client pre-flight\n // (used by the docs demo to run in direct mode without a configured spec).\n allowlist?: AllowlistEntry[];\n snippetLanguages?: SnippetLanguage[];\n}\n\nfunction renderPath(path: string): React.ReactNode {\n return path\n .split(/(\\{[^}]+\\})/g)\n .map((part, index) =>\n part.startsWith(\"{\") ? (\n {part}\n ) : (\n {part}\n ),\n );\n}\n\n/* ---------- compact bar ---------- */\n\nconst CompactBar = styled.div`\n display: flex;\n align-items: center;\n gap: 10px;\n margin: 0;\n padding: 8px 8px 8px 12px;\n border: solid 1px ${({ theme }: { theme: Theme }) => theme.colors.grayLight};\n border-radius: ${({ theme }: { theme: Theme }) => theme.spacing.radius.lg};\n background: ${({ theme }: { theme: Theme }) => theme.colors.light};\n`;\n\n// The method chip is Badge's mono+solid variation; only bar-layout metrics\n// are adjusted here.\nconst MethodBadge = styled(Badge)`\n flex-shrink: 0;\n padding: 0 4px;\n border: none;\n`;\n\nconst PathText = styled.span`\n flex: 1;\n min-width: 0;\n font-family: ${({ theme }: { theme: Theme }) => theme.fonts.mono};\n font-weight: 600;\n font-size: 14px;\n color: ${({ theme }: { theme: Theme }) => theme.colors.dark};\n overflow-x: auto;\n white-space: nowrap;\n &::-webkit-scrollbar {\n display: none;\n }\n`;\n\nconst PathParam = styled.span`\n font-family: ${({ theme }: { theme: Theme }) => theme.fonts.mono};\n background: ${({ theme }: { theme: Theme }) =>\n `color-mix(in srgb, ${theme.colors.primaryLight} 20%, transparent)`};\n color: ${({ theme }: { theme: Theme }) => theme.colors.dark};\n padding: 2px 4px;\n border-radius: ${({ theme }: { theme: Theme }) => theme.spacing.radius.xs};\n overflow-wrap: anywhere;\n`;\n\nconst AccentButton = styled.button<{ theme: Theme }>`\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n flex-shrink: 0;\n font-family: ${({ theme }: { theme: Theme }) => theme.fonts.text};\n font-weight: 600;\n font-size: 14px;\n color: ${({ theme }: { theme: Theme }) => theme.colors.surface};\n background: ${({ theme }: { theme: Theme }) => theme.colors.primary};\n border: none;\n padding: 8px 14px;\n border-radius: ${({ theme }: { theme: Theme }) => theme.spacing.radius.xs};\n ${interactiveStyles};\n\n & svg {\n color: ${({ theme }: { theme: Theme }) => theme.colors.surface};\n stroke: ${({ theme }: { theme: Theme }) => theme.colors.surface};\n }\n\n &:hover {\n background: ${({ theme }: { theme: Theme }) => theme.colors.primaryDark};\n }\n &:disabled {\n opacity: 0.6;\n cursor: default;\n }\n`;\n\n/* ---------- modal ---------- */\n\nconst DialogHeader = styled.div`\n display: flex;\n align-items: center;\n gap: 10px;\n flex-wrap: wrap;\n margin: 5px 0 0 0;\n`;\n\nconst PathBar = styled.div`\n flex: 1;\n min-width: 200px;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 8px 8px 12px;\n border: solid 1px ${({ theme }: { theme: Theme }) => theme.colors.grayLight};\n border-radius: ${({ theme }: { theme: Theme }) => theme.spacing.radius.xs};\n background: ${({ theme }: { theme: Theme }) => theme.colors.light};\n`;\n\nconst DialogBody = styled.div`\n display: grid;\n grid-template-columns: 1fr;\n gap: 20px;\n padding: 0;\n margin: 20px 0 0 0;\n\n ${mq(\"lg\")} {\n grid-template-columns: 1fr 1fr;\n }\n`;\n\nconst Column = styled.div`\n display: flex;\n flex-direction: column;\n gap: 20px;\n min-width: 0;\n`;\n\nconst Description = styled.p`\n margin: 0;\n ${({ theme }: { theme: Theme }) => styledSmall(theme)};\n color: ${({ theme }: { theme: Theme }) => theme.colors.grayDark};\n`;\n\nconst Toolbar = styled.div`\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n`;\n\nconst Toggle = styled.button<{ theme: Theme; $active?: boolean }>`\n position: relative;\n cursor: pointer;\n text-decoration: none;\n font-size: ${({ theme }) => theme.fontSizes.small.lg};\n line-height: 1;\n color: ${({ theme }) => theme.colors.accent};\n display: flex;\n gap: 6px;\n transition: all 0.3s ease;\n font-weight: 600;\n white-space: nowrap;\n min-width: fit-content;\n background: ${({ theme }) =>\n `color-mix(in srgb, ${theme.colors.primaryLight} 10%, transparent)`};\n padding: 6px 8px;\n border: none;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n ${interactiveStyles};\n\n ${({ $active, theme }) =>\n $active &&\n css`\n background: ${theme.colors.primary};\n color: ${theme.colors.surface};\n `}\n`;\n\n/* ---------- collapsible card ---------- */\n\nconst Card = styled.div`\n border: solid 1px ${({ theme }: { theme: Theme }) => theme.colors.grayLight};\n border-radius: ${({ theme }: { theme: Theme }) => theme.spacing.radius.lg};\n overflow: hidden;\n`;\n\nconst CardHead = styled.button`\n cursor: pointer;\n width: 100%;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 8px;\n padding: 12px 20px;\n border: none;\n background: transparent;\n color: ${({ theme }: { theme: Theme }) => theme.colors.dark};\n ${({ theme }: { theme: Theme }) => styledSmall(theme)};\n`;\n\nconst Inline = styled.span`\n display: inline-flex;\n align-items: center;\n gap: 8px;\n`;\n\nconst CardBody = styled.div`\n padding: 0 20px 20px;\n display: flex;\n flex-direction: column;\n gap: 16px;\n`;\n\n/* ---------- fields ---------- */\n\n// Wraps an accordion section's content and insets it, so full-width inputs'\n// focus rings (a 4px box-shadow) are not clipped by the accordion's own\n// overflow. Keeps the accordion component untouched.\nconst SectionInner = styled.div`\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding: 4px;\n`;\n\nconst Field = styled.div`\n display: flex;\n flex-direction: column;\n gap: 6px;\n`;\n\nconst FieldHead = styled.div`\n display: flex;\n align-items: center;\n gap: 8px;\n flex-wrap: wrap;\n ${({ theme }: { theme: Theme }) => styledSmall(theme)};\n`;\n\nconst FieldName = styled.span`\n font-family: ${({ theme }: { theme: Theme }) => theme.fonts.mono};\n font-weight: 600;\n color: ${({ theme }: { theme: Theme }) => theme.colors.dark};\n`;\n\nconst TypeChip = styled.span`\n font-family: ${({ theme }: { theme: Theme }) => theme.fonts.mono};\n color: ${({ theme }: { theme: Theme }) => theme.colors.grayDark};\n background: ${({ theme }: { theme: Theme }) =>\n `color-mix(in srgb, ${theme.colors.gray} 15%, transparent)`};\n padding: 0 4px;\n border-radius: ${({ theme }: { theme: Theme }) => theme.spacing.radius.xs};\n`;\n\nconst RequiredChip = styled.span`\n font-family: ${({ theme }: { theme: Theme }) => theme.fonts.mono};\n color: ${({ theme }: { theme: Theme }) => theme.colors.error};\n`;\n\nconst FieldDesc = styled.p`\n margin: 0;\n width: 100%;\n ${({ theme }: { theme: Theme }) => styledSmall(theme)};\n color: ${({ theme }: { theme: Theme }) => theme.colors.gray};\n`;\n\n/* ---------- response ---------- */\n\nconst StatusPill = styled.span<{ theme: Theme; $ok: boolean }>`\n font-family: ${({ theme }) => theme.fonts.mono};\n font-weight: 700;\n color: ${({ theme }) => theme.colors.surface};\n background: ${({ theme, $ok }) =>\n $ok ? theme.colors.success : theme.colors.error};\n padding: 0 4px;\n border-radius: ${({ theme }) => theme.spacing.radius.xs};\n`;\n\nconst Meta = styled.span`\n ${({ theme }: { theme: Theme }) => styledSmall(theme)};\n color: ${({ theme }: { theme: Theme }) => theme.colors.gray};\n`;\n\nconst Placeholder = styled.div`\n border: dashed 1px ${({ theme }: { theme: Theme }) => theme.colors.grayLight};\n border-radius: ${({ theme }: { theme: Theme }) => theme.spacing.radius.lg};\n padding: 24px;\n min-height: 120px;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 10px;\n text-align: center;\n ${({ theme }: { theme: Theme }) => styledSmall(theme)};\n color: ${({ theme }: { theme: Theme }) => theme.colors.gray};\n`;\n\n// Holds the response (or empty placeholder) and lets the loading spinner sit on\n// top of it, so a re-send keeps the previous content in place instead of\n// collapsing and jumping the layout.\nconst ResponseArea = styled.div`\n position: relative;\n`;\n\nconst LoadingOverlay = styled.div`\n position: absolute;\n inset: 0;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n border: solid 1px ${({ theme }: { theme: Theme }) => theme.colors.grayLight};\n border-radius: ${({ theme }: { theme: Theme }) => theme.spacing.radius.lg};\n color: ${({ theme }: { theme: Theme }) => theme.colors.grayDark};\n background: ${({ theme }: { theme: Theme }) =>\n `color-mix(in srgb, ${theme.colors.light} 60%, transparent)`};\n backdrop-filter: blur(1px);\n`;\n\nconst ResponseMedia = styled.img`\n max-width: 100%;\n border-radius: ${({ theme }: { theme: Theme }) => theme.spacing.radius.xs};\n`;\n\nexport function ApiPlayground({\n operation,\n defaultMode = \"proxy\",\n allowDirect = true,\n allowlist,\n snippetLanguages = [\"curl\", \"javascript\", \"python\"],\n}: ApiPlaygroundProps) {\n const [open, setOpen] = useState(false);\n const [paramValues, setParamValues] = useState>({});\n const [authValues, setAuthValues] = useState>({});\n const initialBody = useMemo(() => firstJsonExample(operation), [operation]);\n const [bodyText, setBodyText] = useState(initialBody?.text ?? \"\");\n const [mode, setMode] = useState<\"proxy\" | \"direct\">(defaultMode);\n const [showSecrets, setShowSecrets] = useState(false);\n const [loading, setLoading] = useState(false);\n const [response, setResponse] = useState(null);\n const [error, setError] = useState(null);\n const [corsRetry, setCorsRetry] = useState(false);\n const abortRef = useRef(null);\n\n const paramKey = (p: ParameterDescriptor) => p.in + \":\" + p.name;\n\n const isAllowed = useCallback(\n (url: string) =>\n Boolean(\n matchAllowlist(url) ??\n (allowlist ? matchAllowlistIn(allowlist, url) : null),\n ),\n [allowlist],\n );\n\n const buildRequest = useCallback((): BuiltRequest => {\n const base = serverBase(operation.servers[0]);\n const pathValues: Record = {};\n const queryParts: string[] = [];\n const headers: Record = {};\n const secretHeaderNames: string[] = [];\n let cookie = \"\";\n\n for (const param of operation.parameters) {\n const value = paramValues[paramKey(param)];\n if (value === undefined || value === \"\") continue;\n if (param.in === \"path\") pathValues[param.name] = value;\n else if (param.in === \"query\")\n queryParts.push(\n encodeURIComponent(param.name) + \"=\" + encodeURIComponent(value),\n );\n else if (param.in === \"header\") headers[param.name] = value;\n else if (param.in === \"cookie\")\n cookie += (cookie ? \"; \" : \"\") + param.name + \"=\" + value;\n }\n\n for (const requirement of operation.security) {\n const scheme = requirement.scheme;\n if (!scheme) continue;\n if (scheme.type === \"apiKey\" && scheme.name) {\n const value = authValues[requirement.schemeName];\n if (!value) continue;\n if (scheme.in === \"header\") {\n headers[scheme.name] = value;\n secretHeaderNames.push(scheme.name);\n } else if (scheme.in === \"query\") {\n queryParts.push(\n encodeURIComponent(scheme.name) + \"=\" + encodeURIComponent(value),\n );\n } else if (scheme.in === \"cookie\") {\n cookie += (cookie ? \"; \" : \"\") + scheme.name + \"=\" + value;\n }\n } else if (scheme.type === \"http\" && scheme.scheme === \"basic\") {\n const user = authValues[requirement.schemeName + \":user\"] ?? \"\";\n const pass = authValues[requirement.schemeName + \":pass\"] ?? \"\";\n if (user || pass) {\n headers[\"Authorization\"] = \"Basic \" + btoa(user + \":\" + pass);\n secretHeaderNames.push(\"Authorization\");\n }\n } else {\n const token = authValues[requirement.schemeName];\n if (token) {\n headers[\"Authorization\"] = \"Bearer \" + token;\n secretHeaderNames.push(\"Authorization\");\n }\n }\n }\n\n if (cookie) headers[\"Cookie\"] = cookie;\n\n const hasBody =\n operation.requestBody !== undefined &&\n operation.method !== \"get\" &&\n operation.method !== \"head\" &&\n bodyText.trim() !== \"\";\n if (hasBody && !headers[\"Content-Type\"] && initialBody) {\n headers[\"Content-Type\"] = initialBody.contentType;\n }\n\n const query = queryParts.join(\"&\");\n const url =\n base +\n applyPathParams(operation.path, pathValues) +\n (query ? \"?\" + query : \"\");\n\n return { url, headers, body: hasBody ? bodyText : null, secretHeaderNames };\n }, [operation, paramValues, authValues, bodyText, initialBody]);\n\n const snippets = useMemo(() => {\n const built = buildRequest();\n const headers: Record = { ...built.headers };\n if (!showSecrets) {\n for (const name of built.secretHeaderNames) {\n headers[name] = \"\";\n }\n }\n return buildSnippets(\n {\n method: operation.method.toUpperCase(),\n url: built.url || operation.path,\n headers,\n body: built.body,\n },\n snippetLanguages,\n );\n }, [buildRequest, showSecrets, operation, snippetLanguages]);\n\n const sendProxy = useCallback(\n async (\n built: BuiltRequest,\n signal: AbortSignal,\n ): Promise => {\n const res = await fetch(\"/api/playground\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n signal,\n body: JSON.stringify({\n targetUrl: built.url,\n method: operation.method.toUpperCase(),\n headers: built.headers,\n body: built.body ?? undefined,\n bodyEncoding: \"utf8\",\n }),\n });\n const data = await res.json();\n if (!res.ok) {\n throw new Error(\n typeof data?.error === \"string\" ? data.error : \"Request failed\",\n );\n }\n return data as PlaygroundResponse;\n },\n [operation],\n );\n\n const sendDirect = useCallback(\n async (\n built: BuiltRequest,\n signal: AbortSignal,\n ): Promise => {\n const start = Date.now();\n const res = await fetch(built.url, {\n method: operation.method.toUpperCase(),\n headers: built.headers,\n body: built.body ?? undefined,\n signal,\n });\n const buffer = await res.arrayBuffer();\n const contentType = res.headers.get(\"content-type\");\n const isText = classifyResponse(contentType) === \"text\";\n const headers: Record = {};\n res.headers.forEach((value, key) => {\n headers[key] = value;\n });\n return {\n status: res.status,\n statusText: res.statusText,\n headers,\n contentType,\n bodyEncoding: isText ? \"utf8\" : \"base64\",\n body: isText\n ? new TextDecoder().decode(buffer)\n : bufferToBase64(buffer),\n truncated: false,\n byteLength: buffer.byteLength,\n durationMs: Date.now() - start,\n };\n },\n [operation],\n );\n\n const send = useCallback(async () => {\n setError(null);\n setCorsRetry(false);\n // Keep any previous response on screen; the loading overlay covers it so\n // the layout does not collapse and jump while the new request is in flight.\n\n const built = buildRequest();\n if (!built.url) {\n setError(\"No server URL is configured for this endpoint.\");\n return;\n }\n if (!isAllowed(built.url)) {\n setError(\"This endpoint's server is not in the allowed list.\");\n return;\n }\n if (\n mode === \"direct\" &&\n typeof window !== \"undefined\" &&\n window.location.protocol === \"https:\" &&\n built.url.startsWith(\"http:\")\n ) {\n setError(\n \"Browsers block HTTP requests from an HTTPS page. Use proxy mode.\",\n );\n return;\n }\n\n const controller = new AbortController();\n abortRef.current = controller;\n setLoading(true);\n try {\n const result =\n mode === \"direct\"\n ? await sendDirect(built, controller.signal)\n : await sendProxy(built, controller.signal);\n setResponse(result);\n } catch (err) {\n if (controller.signal.aborted) return;\n if (mode === \"direct\" && err instanceof TypeError) {\n setCorsRetry(true);\n setError(\n \"The browser blocked this cross-origin request (CORS). Run it server-side instead.\",\n );\n } else {\n setError(err instanceof Error ? err.message : \"Request failed\");\n }\n } finally {\n setLoading(false);\n abortRef.current = null;\n }\n }, [buildRequest, isAllowed, mode, sendDirect, sendProxy]);\n\n const retryViaProxy = useCallback(() => {\n setMode(\"proxy\");\n setCorsRetry(false);\n void send();\n }, [send]);\n\n const groupedParams = LOCATIONS.map((location) => ({\n location,\n params: operation.parameters.filter((p) => p.in === location),\n })).filter((group) => group.params.length > 0);\n // Only the first visible section starts expanded.\n const hasAuth = operation.security.length > 0;\n\n const responseKind = response ? classifyResponse(response.contentType) : null;\n const mediaSrc =\n response && responseKind && responseKind !== \"text\"\n ? \"data:\" +\n (response.contentType ?? \"application/octet-stream\") +\n \";base64,\" +\n response.body\n : \"\";\n\n const renderParamField = (param: ParameterDescriptor) => (\n \n \n {param.name}\n {schemaTypeLabel(param.schema)}\n {param.required ? required : null}\n \n {param.description ? {param.description} : null}\n \n setParamValues((prev) => ({\n ...prev,\n [paramKey(param)]: e.target.value,\n }))\n }\n />\n \n );\n\n return (\n <>\n \n \n {operation.method}\n \n {renderPath(operation.path)}\n \n setOpen(true)}>\n Try it\n \n \n \n\n setOpen(false)}\n $width={1120}\n $title={\n operation.summary ??\n operation.method.toUpperCase() + \" \" + operation.path\n }\n >\n \n \n \n {operation.method}\n \n {renderPath(operation.path)}\n void send()}\n disabled={loading}\n >\n Send\n {loading ? : }\n \n \n \n\n \n \n {operation.description || operation.summary ? (\n \n {operation.description ?? operation.summary}\n \n ) : null}\n\n \n {operation.servers.length > 0 ? (\n {serverBase(operation.servers[0])}\n ) : null}\n {allowDirect ? (\n <>\n setMode(\"proxy\")}\n >\n Proxy\n \n setMode(\"direct\")}\n >\n Direct\n \n >\n ) : null}\n setShowSecrets((v) => !v)}\n >\n {showSecrets ? \"Hide secrets\" : \"Show secrets\"}\n \n \n\n {operation.security.length > 0 ? (\n \n \n {operation.security.map((requirement) => {\n const scheme = requirement.scheme;\n if (\n scheme &&\n scheme.type === \"http\" &&\n scheme.scheme === \"basic\"\n ) {\n return (\n \n \n {requirement.schemeName}\n basic\n \n \n setAuthValues((prev) => ({\n ...prev,\n [requirement.schemeName + \":user\"]:\n e.target.value,\n }))\n }\n />\n \n setAuthValues((prev) => ({\n ...prev,\n [requirement.schemeName + \":pass\"]:\n e.target.value,\n }))\n }\n />\n \n );\n }\n const chip =\n scheme?.type === \"apiKey\"\n ? \"apiKey\"\n : scheme?.scheme === \"bearer\"\n ? \"bearer\"\n : (scheme?.type ?? \"token\");\n return (\n \n \n {requirement.schemeName}\n {chip}\n required\n \n \n setAuthValues((prev) => ({\n ...prev,\n [requirement.schemeName]: e.target.value,\n }))\n }\n />\n \n );\n })}\n \n \n ) : null}\n\n {groupedParams.map((group, index) => (\n \n \n {group.params.map(renderParamField)}\n \n \n ))}\n\n {operation.requestBody &&\n operation.method !== \"get\" &&\n operation.method !== \"head\" ? (\n \n \n \n \n ) : null}\n\n {error ? (\n \n \n {error}
\n \n {corsRetry ? (\n \n Retry via proxy\n \n ) : null}\n \n ) : null}\n \n\n \n \n {response ? (\n \n \n \n = 200 && response.status < 400}\n >\n {response.status} {response.statusText}\n \n {response.durationMs}ms\n {response.byteLength} bytes\n {response.truncated ? (truncated) : null}\n \n \n \n {responseKind === \"image\" ? (\n \n ) : responseKind === \"video\" ? (\n \n ) : responseKind === \"audio\" ? (\n \n ) : responseKind === \"binary\" ? (\n \n Download response ({response.byteLength} bytes)\n \n ) : (\n \n )}\n \n \n ) : (\n \n Send the request to see the live response here.\n \n )}\n {loading ? (\n \n \n \n ) : null}\n \n\n {snippets.length > 0 ? : null}\n \n \n \n >\n );\n}\n";