From 3e2583fdc5680b5ac784d59f0c993730cef73dee Mon Sep 17 00:00:00 2001 From: Rishabh Date: Mon, 11 May 2026 12:06:39 +0530 Subject: [PATCH 01/15] feat: add read-only API overview page (api-v2) New api-v2 components replace EndpointPage with read-only view showing field names and types without editable inputs. Playground dialog and navbar actions to follow in subsequent commits. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../chronicle/src/components/api-v2/PLAN.md | 220 ++++++++++++++++++ .../api-v2/api-field-list.module.css | 76 ++++++ .../src/components/api-v2/api-field-list.tsx | 85 +++++++ .../components/api-v2/api-overview.module.css | 51 ++++ .../src/components/api-v2/api-overview.tsx | 174 ++++++++++++++ .../chronicle/src/components/api-v2/index.ts | 2 + packages/chronicle/src/pages/ApiPage.tsx | 4 +- 7 files changed, 610 insertions(+), 2 deletions(-) create mode 100644 packages/chronicle/src/components/api-v2/PLAN.md create mode 100644 packages/chronicle/src/components/api-v2/api-field-list.module.css create mode 100644 packages/chronicle/src/components/api-v2/api-field-list.tsx create mode 100644 packages/chronicle/src/components/api-v2/api-overview.module.css create mode 100644 packages/chronicle/src/components/api-v2/api-overview.tsx create mode 100644 packages/chronicle/src/components/api-v2/index.ts diff --git a/packages/chronicle/src/components/api-v2/PLAN.md b/packages/chronicle/src/components/api-v2/PLAN.md new file mode 100644 index 00000000..68a70c0d --- /dev/null +++ b/packages/chronicle/src/components/api-v2/PLAN.md @@ -0,0 +1,220 @@ +# API Reference Page Redesign — TODO + +## Context + +Current `EndpointPage` combines read-only docs + editable "try it out" in one view. Redesign separates: +- **Overview page**: read-only, field names + types only +- **Playground dialog**: opens via navbar "Test request" button, all editable fields + live response +- **Navbar**: 3 action buttons (Test request, View documentation, Open in ChatGPT) + +Build all new components from scratch in `components/api-v2/`. Old components stay untouched. + +--- + +## Phase 1: Read-only API Overview + +### TODO +- [ ] Create `api-overview.tsx` + `api-overview.module.css` +- [ ] Create `api-field-list.tsx` + `api-field-list.module.css` + +### `ApiOverview` — main page component + +Two-column grid layout: + +**Left column:** +- Title (h1, `--rs-font-size-t3`) +- Description (secondary color text) +- Method badge + path + copy icon (no Send button) +- **Authorisations** section: `Badge(name)` + "String" type text +- **Query Parameters** section: per field `Badge(name)` + type + description, bottom border separator +- **Response** section: "Response" header + "application/json" + status dropdown (200), description ("OK"), fields same format, "Show child attributes" expandable for nested objects + +**Right column:** +- Code snippet (cURL, Python, Go, TS) with language dropdown + copy +- Response JSON with status code tabs (200, 400, 404, 500) + copy + +### `ApiFieldList` — read-only field display + +Each field renders: +``` +[Badge: field_name] type_text +description (optional) +─── border-bottom ─── +``` + +Nested objects: "Show child attributes" expandable row (bg secondary, border, chevron icon). + +### Reuse (import): +- `MethodBadge` from `components/api/method-badge` +- `flattenSchema`, `generateExampleJson`, `SchemaField` from `lib/schema` + +### Copy (internalize): +- `paramsToFields`, `getRequestBody`, `getResponseSections` helper functions from `endpoint-page.tsx` + +--- + +## Phase 2: Playground Dialog + +### TODO +- [ ] Create `playground-dialog.tsx` + `playground-dialog.module.css` +- [ ] Create `playground-field-row.tsx` + `playground-field-row.module.css` + +### `PlaygroundDialog` — full editable playground + +Uses Apsara `Dialog` (`open`/`onOpenChange` controlled, ~900px width). + +**Structure:** +``` +┌─ Action Nav ──────────────────────────────────────┐ +│ [Breadcrumb: endpoint name] [Reset] [Close] │ +├─ Split panel (flex 1:1) ──────────────────────────┤ +│ Left Panel │ Right Panel │ +│ │ │ +│ "Test request" [JSON]│ "Response" [Body ▾] │ +│ [All] [Auth] [Body] │ Status:200 | Time:987ms │ +│ │ [Curl ▾] │ +│ ┌Authorization ▾┐ │ │ +│ │ Authorization │ │ (code snippet w/ line nos) │ +│ │ [input field] │ │ │ +│ ├Body ▾┤ │ │ +│ │ Name [input] │ │ │ +│ │ Description ... │ │ │ +│ │ Nested arrays │ │ │ +│ └─────────────────┘ │ │ +├─ Bottom bar ──────────────────────────────────────┤ +│ [POST badge] /v0/projects [copy] [Send ▶] │ +└───────────────────────────────────────────────────┘ +``` + +**Action Nav:** +- Breadcrumb showing endpoint name (left) +- Reset icon button + Close (X) icon button (right) + +**Left panel:** +- Tab bar: All | Auth | Body — plain underline tabs (active = border-bottom emphasis, not Apsara Tabs) +- Collapsible sections: gray bg header row (label + chevron), content below +- **Authorization section**: label "Authorization" + `InputField` (24px height, placeholder "Enter ID") +- **Body section**: header has label + JSON toggle (`` icon) + chevron + - Each body field: label (11px medium) left, `InputField` right (168px width) + - Nested arrays: dashed left border, indented children, add (+) / remove (X) / collapse (chevron) icons + +**Right panel:** +- "Response" header + Body dropdown button +- Status bar (gray bg): `Status: {code}` (green) | separator | `Time: {ms}` (green) | Curl dropdown +- Code snippet: line numbers (gray, right-aligned) + response body (monospace, red/syntax colored) + +**Bottom bar:** +- Rounded bordered container: Badge (method) + monospace path + copy icon +- "Send" button: `variant="solid" color="accent" size="small" trailingIcon={PlayIcon}` + +**State** (all `useState` inside dialog): +- `customHeaders`, `headerValues`, `pathValues`, `queryValues` +- `bodyValues`, `bodyJsonStr` (two-way sync) +- `responseBody`, `loading`, `responseTime` +- `activeTab` (All/Auth/Body) +- `collapsedSections` (Set of section names) +- `jsonView` (boolean for body JSON toggle) + +**Send handler:** POST to `/api/apis-proxy` with `{ specName, method, path, headers, body }`. Track response time. + +### `PlaygroundFieldRow` — editable field row + +Layout: `label (flex-1) | InputField (168px)` +- 11px medium font for label +- InputField 24px height, 12px font, placeholder text +- For nested: dashed left border, indented, X button to remove + +--- + +## Phase 3: Navbar Action Buttons + +### TODO +- [ ] Create `api-nav-actions.tsx` + `api-nav-actions.module.css` + +### `ApiNavActions` + +Renders 3 buttons + hosts `PlaygroundDialog`: + +1. **Test request** — `Button variant="outline" color="neutral" size="small" leadingIcon={PlayIcon}` → opens playground dialog +2. **View documentation** — `Button variant="outline" color="neutral" size="small" leadingIcon={DocumentIcon}` → navigates to docs page +3. **Open in ChatGPT** — `Button variant="outline" color="neutral" size="small" leadingIcon={ChatGPTIcon} trailingIcon={ChevronDownIcon}` → dropdown menu with: Copy as MD, View MD, Open in ChatGPT, Open in Claude (same as existing `OpenInAI`) + +Reads `apiOperation` from page context to pass to `PlaygroundDialog`. + +Manages `playgroundOpen` state locally. + +--- + +## Phase 4: Wire Together + +### TODO +- [ ] Update `lib/page-context.tsx` — add `apiOperation` field +- [ ] Update `pages/ApiPage.tsx` — import `ApiOverview` instead of `EndpointPage`, set `apiOperation` in context +- [ ] Update `themes/default/Layout.tsx` — render `ApiNavActions` on API endpoint pages (when `apiOperation` exists in context) +- [ ] Create `components/api-v2/index.ts` — barrel exports + +### `page-context.tsx` changes + +Add to context type: +```ts +apiOperation?: { + method: string + path: string + operation: OpenAPIV3.OperationObject + serverUrl: string + specName: string + auth?: { type: string; header: string; placeholder?: string } +} +``` + +### `ApiPage.tsx` changes + +```tsx +// Before: +// After: set apiOperation in context + +``` + +### `Layout.tsx` changes + +```tsx +// Line 193 area: +{apiOperation ? : } +``` + +--- + +## File Summary + +| File | Action | +|------|--------| +| `components/api-v2/api-overview.tsx` | **NEW** | +| `components/api-v2/api-overview.module.css` | **NEW** | +| `components/api-v2/api-field-list.tsx` | **NEW** | +| `components/api-v2/api-field-list.module.css` | **NEW** | +| `components/api-v2/playground-dialog.tsx` | **NEW** | +| `components/api-v2/playground-dialog.module.css` | **NEW** | +| `components/api-v2/playground-field-row.tsx` | **NEW** | +| `components/api-v2/playground-field-row.module.css` | **NEW** | +| `components/api-v2/api-nav-actions.tsx` | **NEW** | +| `components/api-v2/api-nav-actions.module.css` | **NEW** | +| `components/api-v2/index.ts` | **NEW** | +| `lib/page-context.tsx` | Modify — add `apiOperation` | +| `pages/ApiPage.tsx` | Modify — use new components | +| `themes/default/Layout.tsx` | Modify — conditional navbar | + +**Untouched**: all existing `components/api/` files. + +--- + +## Verification + +1. `bun run build:cli` +2. `bun run dev:examples:basic` → API reference pages +3. Overview page: read-only (Badge + type), two-column, no inputs +4. Navbar: 3 buttons on API endpoint pages +5. "Test request" → dialog opens with editable fields +6. Fill + Send → response shows (status, time, JSON) +7. Close → back to overview +8. "View documentation" + "Open in ChatGPT" work +9. Non-API pages: `` only +10. `bunx tsc --noEmit --project packages/chronicle/tsconfig.json` passes diff --git a/packages/chronicle/src/components/api-v2/api-field-list.module.css b/packages/chronicle/src/components/api-v2/api-field-list.module.css new file mode 100644 index 00000000..5037c985 --- /dev/null +++ b/packages/chronicle/src/components/api-v2/api-field-list.module.css @@ -0,0 +1,76 @@ +.sectionTitle { + font-size: var(--rs-font-size-large); + font-weight: var(--rs-font-weight-medium); + line-height: var(--rs-line-height-large); + letter-spacing: var(--rs-letter-spacing-large); + color: var(--rs-color-foreground-base-primary); +} + +.fieldItem { + display: flex; + flex-direction: column; + gap: var(--rs-space-4); + padding-bottom: var(--rs-space-5); + border-bottom: 0.5px solid var(--rs-color-border-base-primary); +} + +.fieldItem:last-child { + border-bottom: none; +} + +.fieldType { + font-size: var(--rs-font-size-small); + line-height: var(--rs-line-height-small); + letter-spacing: var(--rs-letter-spacing-small); + color: var(--rs-color-foreground-base-secondary); +} + +.fieldDescription { + font-size: var(--rs-font-size-small); + line-height: var(--rs-line-height-small); + letter-spacing: var(--rs-letter-spacing-small); + color: var(--rs-color-foreground-base-secondary); +} + +.statusDescription { + font-size: var(--rs-font-size-regular); + line-height: var(--rs-line-height-regular); + color: var(--rs-color-foreground-base-primary); +} + +.contentType { + font-family: var(--rs-font-mono); + font-size: var(--rs-font-size-mono-small); + line-height: var(--rs-line-height-small); + color: var(--rs-color-foreground-base-primary); +} + +.expandButton { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--rs-space-3) var(--rs-space-4); + border: 1px solid var(--rs-color-border-base-primary); + border-radius: var(--rs-radius-2); + background: var(--rs-color-background-base-secondary); + cursor: pointer; + width: 100%; +} + +.expandButton:hover { + background: var(--rs-color-background-neutral-secondary); +} + +.expandLabel { + font-size: var(--rs-font-size-small); + line-height: var(--rs-line-height-small); + color: var(--rs-color-foreground-base-primary); +} + +.childFields { + display: flex; + flex-direction: column; + gap: var(--rs-space-5); + padding-left: var(--rs-space-5); + margin-top: var(--rs-space-3); +} diff --git a/packages/chronicle/src/components/api-v2/api-field-list.tsx b/packages/chronicle/src/components/api-v2/api-field-list.tsx new file mode 100644 index 00000000..e0653505 --- /dev/null +++ b/packages/chronicle/src/components/api-v2/api-field-list.tsx @@ -0,0 +1,85 @@ +'use client' + +import { useState, type ReactNode } from 'react' +import { Badge, Flex, IconButton, Separator } from '@raystack/apsara' +import { ChevronRightIcon, ChevronDownIcon } from '@radix-ui/react-icons' +import type { SchemaField } from '@/lib/schema' +import styles from './api-field-list.module.css' + +interface ApiFieldSectionProps { + title: string + fields: SchemaField[] + headerRight?: ReactNode + description?: string +} + +export function ApiFieldSection({ title, fields, headerRight, description }: ApiFieldSectionProps) { + if (fields.length === 0 && !description) return null + + return ( + + + {title} + {headerRight && ( + + {headerRight} + + )} + + + {description && {description}} + + {fields.map((field) => ( + + ))} + + + ) +} + +function FieldItem({ field }: { field: SchemaField }) { + const hasChildren = field.children && field.children.length > 0 + + return ( +
+ + {field.name} + {field.type} + + {field.description && ( + {field.description} + )} + {hasChildren && } +
+ ) +} + +function ExpandableChildren({ field }: { field: SchemaField }) { + const [expanded, setExpanded] = useState(false) + + return ( + + + {expanded && ( +
+ {field.children!.map((child) => ( + + ))} +
+ )} +
+ ) +} diff --git a/packages/chronicle/src/components/api-v2/api-overview.module.css b/packages/chronicle/src/components/api-v2/api-overview.module.css new file mode 100644 index 00000000..23d58060 --- /dev/null +++ b/packages/chronicle/src/components/api-v2/api-overview.module.css @@ -0,0 +1,51 @@ +.layout { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--rs-space-10); + padding: var(--rs-space-9) var(--rs-space-9) var(--rs-space-9) var(--rs-space-9); + max-width: 1400px; +} + +.left { + min-width: 0; +} + +.right { + min-width: 0; +} + +.title { + font-family: var(--rs-font-title); + font-size: var(--rs-font-size-t3); + font-weight: var(--rs-font-weight-medium); + line-height: var(--rs-line-height-t3); + letter-spacing: var(--rs-letter-spacing-t3); + color: var(--rs-color-foreground-base-primary); + margin: 0; +} + +.description { + font-size: var(--rs-font-size-regular); + line-height: var(--rs-line-height-regular); + letter-spacing: var(--rs-letter-spacing-regular); + color: var(--rs-color-foreground-base-secondary); +} + +.path { + font-family: var(--rs-font-mono); + font-size: var(--rs-font-size-mono-regular); + line-height: var(--rs-line-height-regular); + color: var(--rs-color-foreground-base-primary); +} + +.responseLabel { + font-size: var(--rs-font-size-small); + line-height: var(--rs-line-height-small); + color: var(--rs-color-foreground-base-secondary); +} + +@media (max-width: 900px) { + .layout { + grid-template-columns: 1fr; + } +} diff --git a/packages/chronicle/src/components/api-v2/api-overview.tsx b/packages/chronicle/src/components/api-v2/api-overview.tsx new file mode 100644 index 00000000..b26e612d --- /dev/null +++ b/packages/chronicle/src/components/api-v2/api-overview.tsx @@ -0,0 +1,174 @@ +'use client' + +import type { OpenAPIV3 } from 'openapi-types' +import { Flex, IconButton, Badge, Button } from '@raystack/apsara' +import { CopyIcon, ChevronDownIcon } from '@radix-ui/react-icons' +import { MethodBadge } from '@/components/api/method-badge' +import { CodeSnippets } from '@/components/api/code-snippets' +import { ResponsePanel } from '@/components/api/response-panel' +import { flattenSchema, generateExampleJson, type SchemaField } from '@/lib/schema' +import { ApiFieldSection } from './api-field-list' +import styles from './api-overview.module.css' + +interface ApiOverviewProps { + method: string + path: string + operation: OpenAPIV3.OperationObject + serverUrl: string + specName: string + auth?: { type: string; header: string; placeholder?: string } +} + +export function ApiOverview({ method, path, operation, auth }: ApiOverviewProps) { + const params = (operation.parameters ?? []) as OpenAPIV3.ParameterObject[] + const body = getRequestBody(operation.requestBody as OpenAPIV3.RequestBodyObject | undefined) + + const headerFields = paramsToFields(params.filter((p) => p.in === 'header')) + const pathFields = paramsToFields(params.filter((p) => p.in === 'path')) + const queryFields = paramsToFields(params.filter((p) => p.in === 'query')) + const responses = getResponseSections(operation.responses as Record) + + const authFields: SchemaField[] = auth + ? [{ name: auth.header, type: 'String', required: false }] + : headerFields.length > 0 + ? headerFields + : [] + + const fullUrl = '{domain}' + path + const snippetHeaders: Record = {} + if (auth) snippetHeaders[auth.header] = auth.placeholder ?? 'YOUR_API_KEY' + if (body) snippetHeaders['Content-Type'] = body.contentType ?? 'application/json' + + const copyPath = () => { + void navigator.clipboard.writeText(path) + } + + return ( +
+ + + {operation.summary && ( +

{operation.summary}

+ )} + {operation.description && ( +

{operation.description}

+ )} +
+ + + + {path} + + + + + + {authFields.length > 0 && ( + + )} + + {pathFields.length > 0 && ( + + )} + + {queryFields.length > 0 && ( + + )} + + {body && body.fields.length > 0 && ( + + )} + + {responses.map((resp) => ( + + {resp.contentType && ( + {resp.contentType} + )} + + + } + /> + ))} +
+ + + + + Response: + + + +
+ ) +} + +function paramsToFields(params: OpenAPIV3.ParameterObject[]): SchemaField[] { + return params.map((p) => { + const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject + return { + name: p.name, + type: schema.type ? String(schema.type) : 'string', + required: p.required ?? false, + description: p.description, + default: schema.default, + } + }) +} + +interface RequestBody { + contentType: string + fields: SchemaField[] + jsonExample: string +} + +function getRequestBody(body: OpenAPIV3.RequestBodyObject | undefined): RequestBody | null { + if (!body?.content) return null + const contentType = Object.keys(body.content)[0] + if (!contentType) return null + const schema = body.content[contentType]?.schema as OpenAPIV3.SchemaObject | undefined + if (!schema) return null + return { + contentType, + fields: flattenSchema(schema), + jsonExample: JSON.stringify(generateExampleJson(schema), null, 2), + } +} + +interface ResponseSection { + status: string + description?: string + contentType?: string + fields: SchemaField[] + jsonExample?: string +} + +function getResponseSections(responses: Record): ResponseSection[] { + return Object.entries(responses).map(([status, resp]) => { + const content = resp.content ?? {} + const contentType = Object.keys(content)[0] + const schema = contentType + ? (content[contentType]?.schema as OpenAPIV3.SchemaObject | undefined) + : undefined + + return { + status, + description: resp.description, + contentType, + fields: schema ? flattenSchema(schema) : [], + jsonExample: schema ? JSON.stringify(generateExampleJson(schema), null, 2) : undefined, + } + }) +} diff --git a/packages/chronicle/src/components/api-v2/index.ts b/packages/chronicle/src/components/api-v2/index.ts new file mode 100644 index 00000000..816ea939 --- /dev/null +++ b/packages/chronicle/src/components/api-v2/index.ts @@ -0,0 +1,2 @@ +export { ApiOverview } from './api-overview' +export { ApiFieldSection } from './api-field-list' diff --git a/packages/chronicle/src/pages/ApiPage.tsx b/packages/chronicle/src/pages/ApiPage.tsx index 8143ea24..803271f1 100644 --- a/packages/chronicle/src/pages/ApiPage.tsx +++ b/packages/chronicle/src/pages/ApiPage.tsx @@ -1,6 +1,6 @@ import { Flex, Headline, Text } from '@raystack/apsara'; import type { OpenAPIV3 } from 'openapi-types'; -import { EndpointPage } from '@/components/api'; +import { ApiOverview } from '@/components/api-v2'; import { findApiOperation } from '@/lib/api-routes'; import { Head } from '@/lib/head'; import type { ApiSpec } from '@/lib/openapi'; @@ -36,7 +36,7 @@ export function ApiPage({ slug }: ApiPageProps) { return ( <> - Date: Mon, 11 May 2026 14:09:50 +0530 Subject: [PATCH 02/15] feat: match API overview to Figma design - Layout: flex justify-between with px-32px, matching Figma content node - ApiLayout: override parent content padding for horizontal control - Sections: dividers between sections with proper margin - Fields: Flex gap numbers, Badge micro size, Apsara CopyButton - Code snippet: custom component with CodeBlock highlighting, language dropdown via Menu, title in header - Response panel: custom status tabs (200/400/404/500), CodeBlock for JSON highlighting, shows {} for responses without examples - Typography: all using Apsara design tokens and --rs-font-mono Co-Authored-By: Claude Opus 4.6 (1M context) --- .../api-v2/api-code-snippet.module.css | 29 +++ .../components/api-v2/api-code-snippet.tsx | 64 +++++++ .../api-v2/api-field-list.module.css | 10 +- .../src/components/api-v2/api-field-list.tsx | 23 +-- .../components/api-v2/api-overview.module.css | 66 +++++-- .../src/components/api-v2/api-overview.tsx | 176 +++++++++++------- .../api-v2/api-response-panel.module.css | 79 ++++++++ .../components/api-v2/api-response-panel.tsx | 54 ++++++ .../chronicle/src/components/api-v2/index.ts | 2 + .../chronicle/src/pages/ApiLayout.module.css | 1 + 10 files changed, 405 insertions(+), 99 deletions(-) create mode 100644 packages/chronicle/src/components/api-v2/api-code-snippet.module.css create mode 100644 packages/chronicle/src/components/api-v2/api-code-snippet.tsx create mode 100644 packages/chronicle/src/components/api-v2/api-response-panel.module.css create mode 100644 packages/chronicle/src/components/api-v2/api-response-panel.tsx diff --git a/packages/chronicle/src/components/api-v2/api-code-snippet.module.css b/packages/chronicle/src/components/api-v2/api-code-snippet.module.css new file mode 100644 index 00000000..b7c57544 --- /dev/null +++ b/packages/chronicle/src/components/api-v2/api-code-snippet.module.css @@ -0,0 +1,29 @@ +.container { + border: 0.5px solid var(--rs-color-border-base-primary); + border-radius: var(--rs-radius-2); + overflow: hidden; + width: 100%; +} + +.header { + justify-content: space-between; +} + +.title { + font-size: var(--rs-font-size-regular); + font-weight: var(--rs-font-weight-regular); + line-height: var(--rs-line-height-regular); + letter-spacing: var(--rs-letter-spacing-regular); + color: var(--rs-color-foreground-base-primary); + white-space: nowrap; +} + +.actions { + display: flex; + align-items: center; + gap: var(--rs-space-4); +} + +.body { + background: var(--rs-color-background-base-primary); +} diff --git a/packages/chronicle/src/components/api-v2/api-code-snippet.tsx b/packages/chronicle/src/components/api-v2/api-code-snippet.tsx new file mode 100644 index 00000000..eeaea152 --- /dev/null +++ b/packages/chronicle/src/components/api-v2/api-code-snippet.tsx @@ -0,0 +1,64 @@ +'use client' + +import { useMemo, useState } from 'react' +import { CodeBlock } from '@raystack/apsara' +import { + generateCurl, + generatePython, + generateGo, + generateTypeScript, +} from '@/lib/snippet-generators' +import styles from './api-code-snippet.module.css' + +interface ApiCodeSnippetProps { + title: string + method: string + url: string + headers: Record + body?: string +} + +const languages = [ + { value: 'curl', label: 'cURL', lang: 'bash', generate: generateCurl }, + { value: 'python', label: 'Python', lang: 'python', generate: generatePython }, + { value: 'go', label: 'Go', lang: 'go', generate: generateGo }, + { value: 'typescript', label: 'TypeScript', lang: 'typescript', generate: generateTypeScript }, +] + +export function ApiCodeSnippet({ title, method, url, headers, body }: ApiCodeSnippetProps) { + const [selected, setSelected] = useState('curl') + const current = languages.find((l) => l.value === selected) ?? languages[0] + + const code = useMemo( + () => current.generate({ method, url, headers, body }), + [selected, method, url, headers, body], + ) + + return ( + + + {title} +
+ + + + {languages.map((l) => ( + + {l.label} + + ))} + + + +
+
+ + {code} + +
+ ) +} diff --git a/packages/chronicle/src/components/api-v2/api-field-list.module.css b/packages/chronicle/src/components/api-v2/api-field-list.module.css index 5037c985..38f7f964 100644 --- a/packages/chronicle/src/components/api-v2/api-field-list.module.css +++ b/packages/chronicle/src/components/api-v2/api-field-list.module.css @@ -16,6 +16,7 @@ .fieldItem:last-child { border-bottom: none; + padding-bottom: 0; } .fieldType { @@ -38,13 +39,6 @@ color: var(--rs-color-foreground-base-primary); } -.contentType { - font-family: var(--rs-font-mono); - font-size: var(--rs-font-size-mono-small); - line-height: var(--rs-line-height-small); - color: var(--rs-color-foreground-base-primary); -} - .expandButton { display: flex; align-items: center; @@ -55,6 +49,7 @@ background: var(--rs-color-background-base-secondary); cursor: pointer; width: 100%; + color: var(--rs-color-foreground-base-primary); } .expandButton:hover { @@ -70,7 +65,6 @@ .childFields { display: flex; flex-direction: column; - gap: var(--rs-space-5); padding-left: var(--rs-space-5); margin-top: var(--rs-space-3); } diff --git a/packages/chronicle/src/components/api-v2/api-field-list.tsx b/packages/chronicle/src/components/api-v2/api-field-list.tsx index e0653505..4121f0e0 100644 --- a/packages/chronicle/src/components/api-v2/api-field-list.tsx +++ b/packages/chronicle/src/components/api-v2/api-field-list.tsx @@ -1,7 +1,7 @@ 'use client' import { useState, type ReactNode } from 'react' -import { Badge, Flex, IconButton, Separator } from '@raystack/apsara' +import { Badge, Flex, IconButton } from '@raystack/apsara' import { ChevronRightIcon, ChevronDownIcon } from '@radix-ui/react-icons' import type { SchemaField } from '@/lib/schema' import styles from './api-field-list.module.css' @@ -17,16 +17,15 @@ export function ApiFieldSection({ title, fields, headerRight, description }: Api if (fields.length === 0 && !description) return null return ( - + {title} {headerRight && ( - + {headerRight} )} - {description && {description}} {fields.map((field) => ( @@ -42,12 +41,14 @@ function FieldItem({ field }: { field: SchemaField }) { return (
- + {field.name} {field.type} {field.description && ( - {field.description} + + {field.description} + )} {hasChildren && }
@@ -67,11 +68,11 @@ function ExpandableChildren({ field }: { field: SchemaField }) { {expanded ? 'Hide' : 'Show'} child attributes - - - {expanded ? : } - - + {expanded ? ( + + ) : ( + + )} {expanded && (
diff --git a/packages/chronicle/src/components/api-v2/api-overview.module.css b/packages/chronicle/src/components/api-v2/api-overview.module.css index 23d58060..b5082942 100644 --- a/packages/chronicle/src/components/api-v2/api-overview.module.css +++ b/packages/chronicle/src/components/api-v2/api-overview.module.css @@ -1,17 +1,39 @@ .layout { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--rs-space-10); - padding: var(--rs-space-9) var(--rs-space-9) var(--rs-space-9) var(--rs-space-9); - max-width: 1400px; + display: flex; + align-items: flex-start; + justify-content: space-between; + padding-left: var(--rs-space-9); + padding-right: var(--rs-space-9); + width: 100%; } .left { min-width: 0; + display: flex; + flex-direction: column; + gap: var(--rs-space-10); + flex: 0 1 545px; } .right { - min-width: 0; + min-width: 376px; + max-width: 500px; + display: flex; + flex-direction: column; + gap: var(--rs-space-8); + width: 100%; +} + +.titleBlock { + display: flex; + flex-direction: column; + gap: var(--rs-space-7); +} + +.titleText { + display: flex; + flex-direction: column; + gap: var(--rs-space-4); } .title { @@ -29,6 +51,15 @@ line-height: var(--rs-line-height-regular); letter-spacing: var(--rs-letter-spacing-regular); color: var(--rs-color-foreground-base-secondary); + margin: 0; +} + +.methodBar { + display: flex; + align-items: center; + gap: 8px; + padding: var(--rs-space-3) 0; + border-radius: var(--rs-radius-2); } .path { @@ -38,14 +69,25 @@ color: var(--rs-color-foreground-base-primary); } -.responseLabel { - font-size: var(--rs-font-size-small); - line-height: var(--rs-line-height-small); - color: var(--rs-color-foreground-base-secondary); +.divider { + padding: 0; + margin: var(--rs-space-2) 0; } -@media (max-width: 900px) { +.sections { + display: flex; + flex-direction: column; + gap: var(--rs-space-6); +} + +@media (max-width: 1100px) { .layout { - grid-template-columns: 1fr; + flex-direction: column; + gap: var(--rs-space-9); + } + + .left, + .right { + width: 100%; } } diff --git a/packages/chronicle/src/components/api-v2/api-overview.tsx b/packages/chronicle/src/components/api-v2/api-overview.tsx index b26e612d..d0965b11 100644 --- a/packages/chronicle/src/components/api-v2/api-overview.tsx +++ b/packages/chronicle/src/components/api-v2/api-overview.tsx @@ -1,11 +1,12 @@ 'use client' +import { useState } from 'react' import type { OpenAPIV3 } from 'openapi-types' -import { Flex, IconButton, Badge, Button } from '@raystack/apsara' -import { CopyIcon, ChevronDownIcon } from '@radix-ui/react-icons' +import { Button, Menu, CopyButton, Separator } from '@raystack/apsara' +import { ChevronDownIcon } from '@radix-ui/react-icons' import { MethodBadge } from '@/components/api/method-badge' -import { CodeSnippets } from '@/components/api/code-snippets' -import { ResponsePanel } from '@/components/api/response-panel' +import { ApiCodeSnippet } from './api-code-snippet' +import { ApiResponsePanel } from './api-response-panel' import { flattenSchema, generateExampleJson, type SchemaField } from '@/lib/schema' import { ApiFieldSection } from './api-field-list' import styles from './api-overview.module.css' @@ -39,82 +40,121 @@ export function ApiOverview({ method, path, operation, auth }: ApiOverviewProps) if (auth) snippetHeaders[auth.header] = auth.placeholder ?? 'YOUR_API_KEY' if (body) snippetHeaders['Content-Type'] = body.contentType ?? 'application/json' - const copyPath = () => { - void navigator.clipboard.writeText(path) - } + + const hasSections = authFields.length > 0 || pathFields.length > 0 || + queryFields.length > 0 || (body && body.fields.length > 0) || responses.length > 0 return (
- - - {operation.summary && ( -

{operation.summary}

- )} - {operation.description && ( -

{operation.description}

- )} -
- - - - {path} - - - - - - {authFields.length > 0 && ( - - )} - - {pathFields.length > 0 && ( - - )} - - {queryFields.length > 0 && ( - +
+
+
+ {operation.summary && ( +

{operation.summary}

+ )} + {operation.description && ( +

{operation.description}

+ )} +
+
+ + {path} + +
+
+ + {hasSections && ( +
+ {authFields.length > 0 && ( + + )} + + {authFields.length > 0 && (queryFields.length > 0 || pathFields.length > 0 || (body && body.fields.length > 0) || responses.length > 0) && ( + + )} + + {pathFields.length > 0 && ( + + )} + + {pathFields.length > 0 && (queryFields.length > 0 || (body && body.fields.length > 0) || responses.length > 0) && ( + + )} + + {queryFields.length > 0 && ( + + )} + + {queryFields.length > 0 && ((body && body.fields.length > 0) || responses.length > 0) && ( + + )} + + {body && body.fields.length > 0 && ( + + )} + + {body && body.fields.length > 0 && responses.length > 0 && ( + + )} + + {responses.length > 0 && ( + + )} +
)} +
- {body && body.fields.length > 0 && ( - - )} - - {responses.map((resp) => ( - - {resp.contentType && ( - {resp.contentType} - )} - - - } - /> - ))} -
- - - + - - Response: - - - + +
) } +function ResponseSection({ responses }: { responses: ResponseSectionData[] }) { + const [selectedStatus, setSelectedStatus] = useState(responses[0]?.status ?? '200') + const active = responses.find((r) => r.status === selectedStatus) ?? responses[0] + if (!active) return null + + return ( + + {active.contentType && ( + {active.contentType} + )} + + } /> + } + > + {active.status} + + + {responses.map((resp) => ( + setSelectedStatus(resp.status)}> + {resp.status}{resp.description ? ` — ${resp.description}` : ''} + + ))} + + + + } + /> + ) +} + function paramsToFields(params: OpenAPIV3.ParameterObject[]): SchemaField[] { return params.map((p) => { const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject @@ -147,7 +187,7 @@ function getRequestBody(body: OpenAPIV3.RequestBodyObject | undefined): RequestB } } -interface ResponseSection { +interface ResponseSectionData { status: string description?: string contentType?: string @@ -155,7 +195,7 @@ interface ResponseSection { jsonExample?: string } -function getResponseSections(responses: Record): ResponseSection[] { +function getResponseSections(responses: Record): ResponseSectionData[] { return Object.entries(responses).map(([status, resp]) => { const content = resp.content ?? {} const contentType = Object.keys(content)[0] diff --git a/packages/chronicle/src/components/api-v2/api-response-panel.module.css b/packages/chronicle/src/components/api-v2/api-response-panel.module.css new file mode 100644 index 00000000..ffddc8b9 --- /dev/null +++ b/packages/chronicle/src/components/api-v2/api-response-panel.module.css @@ -0,0 +1,79 @@ +.wrapper { + display: flex; + flex-direction: column; + gap: var(--rs-space-4); +} + +.label { + font-size: var(--rs-font-size-small); + line-height: var(--rs-line-height-small); + letter-spacing: var(--rs-letter-spacing-small); + color: var(--rs-color-foreground-base-primary); +} + +.container { + border: 0.5px solid var(--rs-color-border-base-primary); + border-radius: var(--rs-radius-2); + overflow: hidden; + display: flex; + flex-direction: column; + height: 440px; + width: 100%; +} + +.header { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--rs-space-3) var(--rs-space-5); + background: var(--rs-color-background-base-secondary); + border-bottom: 0.5px solid var(--rs-color-border-base-primary); + flex-shrink: 0; +} + +.tabs { + display: flex; + align-items: center; + gap: var(--rs-space-3); +} + +.tab { + display: flex; + align-items: center; + justify-content: center; + height: 20px; + padding: 0 var(--rs-space-2); + border: 0.5px solid var(--rs-color-border-base-primary); + border-radius: var(--rs-radius-2); + background: transparent; + cursor: pointer; + font-family: var(--rs-font-body); + font-size: var(--rs-font-size-mini); + font-weight: var(--rs-font-weight-medium); + line-height: var(--rs-line-height-mini); + letter-spacing: var(--rs-letter-spacing-mini); + color: var(--rs-color-foreground-base-secondary); +} + +.tab:hover { + background: var(--rs-color-background-neutral-secondary); +} + +.tabActive { + background: var(--rs-color-background-neutral-primary); + border-color: var(--rs-color-border-base-secondary); + color: var(--rs-color-foreground-base-primary); +} + +.body { + flex: 1; + min-height: 0; + overflow-x: hidden; + overflow-y: auto; + background: var(--rs-color-background-base-primary); +} + +.codeBlock { + border: none; + border-radius: 0; +} diff --git a/packages/chronicle/src/components/api-v2/api-response-panel.tsx b/packages/chronicle/src/components/api-v2/api-response-panel.tsx new file mode 100644 index 00000000..b700b16a --- /dev/null +++ b/packages/chronicle/src/components/api-v2/api-response-panel.tsx @@ -0,0 +1,54 @@ +'use client' + +import { useState } from 'react' +import { CodeBlock, CopyButton } from '@raystack/apsara' +import styles from './api-response-panel.module.css' + +interface ResponseData { + status: string + description?: string + jsonExample?: string +} + +interface ApiResponsePanelProps { + responses: ResponseData[] +} + +export function ApiResponsePanel({ responses }: ApiResponsePanelProps) { + const [selected, setSelected] = useState(responses[0]?.status ?? '') + + if (responses.length === 0) return null + + const active = responses.find((r) => r.status === selected) ?? responses[0] + const displayJson = active.jsonExample ?? '{}' + + return ( +
+ Response: +
+
+
+ {responses.map((resp) => ( + + ))} +
+ +
+
+ + + {displayJson} + + +
+
+
+ ) +} diff --git a/packages/chronicle/src/components/api-v2/index.ts b/packages/chronicle/src/components/api-v2/index.ts index 816ea939..170743b1 100644 --- a/packages/chronicle/src/components/api-v2/index.ts +++ b/packages/chronicle/src/components/api-v2/index.ts @@ -1,2 +1,4 @@ export { ApiOverview } from './api-overview' export { ApiFieldSection } from './api-field-list' +export { ApiCodeSnippet } from './api-code-snippet' +export { ApiResponsePanel } from './api-response-panel' diff --git a/packages/chronicle/src/pages/ApiLayout.module.css b/packages/chronicle/src/pages/ApiLayout.module.css index a3c8ffe0..4943509f 100644 --- a/packages/chronicle/src/pages/ApiLayout.module.css +++ b/packages/chronicle/src/pages/ApiLayout.module.css @@ -14,6 +14,7 @@ .content { height: 100%; overflow-y: auto; + padding-left: 0; padding-right: 0; } From c907c08cab716e2b0e15adf0fe17719b4cd1e249 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Mon, 11 May 2026 14:21:21 +0530 Subject: [PATCH 03/15] feat: add View documentation button in API navbar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Read externalDocs URL from OpenAPI operation, fallback to spec level - Preserve externalDocs in Swagger 2.0 → OpenAPI 3.0 conversion - Shared useApiOperation() hook resolves current endpoint from URL - Button shows in navbar only on API endpoint pages with docs URL Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/lib/openapi.ts | 1 + .../chronicle/src/lib/use-api-operation.ts | 15 ++++++++++ .../chronicle/src/themes/default/Layout.tsx | 30 +++++++++++++++++-- 3 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 packages/chronicle/src/lib/use-api-operation.ts diff --git a/packages/chronicle/src/lib/openapi.ts b/packages/chronicle/src/lib/openapi.ts index 0d8a85c6..6e12e78b 100644 --- a/packages/chronicle/src/lib/openapi.ts +++ b/packages/chronicle/src/lib/openapi.ts @@ -125,6 +125,7 @@ function convertV2toV3(doc: OpenAPIV2.Document): OpenAPIV3.Document { info: resolved.info as unknown as OpenAPIV3.InfoObject, paths: v3Paths, tags: (resolved.tags ?? []) as unknown as OpenAPIV3.TagObject[], + ...(resolved.externalDocs ? { externalDocs: resolved.externalDocs as unknown as OpenAPIV3.ExternalDocumentationObject } : {}), } } diff --git a/packages/chronicle/src/lib/use-api-operation.ts b/packages/chronicle/src/lib/use-api-operation.ts new file mode 100644 index 00000000..950def62 --- /dev/null +++ b/packages/chronicle/src/lib/use-api-operation.ts @@ -0,0 +1,15 @@ +import { useMemo } from 'react' +import { useLocation } from 'react-router' +import { findApiOperation, type ApiRouteMatch } from '@/lib/api-routes' +import { usePageContext } from '@/lib/page-context' + +export function useApiOperation(): ApiRouteMatch | null { + const { apiSpecs } = usePageContext() + const { pathname } = useLocation() + + return useMemo(() => { + const slug = pathname.replace(/^\/apis\//, '').split('/').filter(Boolean) + if (slug.length !== 2) return null + return findApiOperation(apiSpecs, slug) + }, [apiSpecs, pathname]) +} diff --git a/packages/chronicle/src/themes/default/Layout.tsx b/packages/chronicle/src/themes/default/Layout.tsx index 00674b7b..dcc2a6ae 100644 --- a/packages/chronicle/src/themes/default/Layout.tsx +++ b/packages/chronicle/src/themes/default/Layout.tsx @@ -6,11 +6,13 @@ import { DocumentTextIcon, Squares2X2Icon } from '@heroicons/react/24/outline'; -import { Flex, IconButton, Sidebar } from '@raystack/apsara'; +import { Flex, IconButton, Button, Sidebar } from '@raystack/apsara'; import { cx } from 'class-variance-authority'; import { useEffect, useMemo, useRef } from 'react'; import { Link as RouterLink, useLocation, useNavigate } from 'react-router'; +import type { OpenAPIV3 } from 'openapi-types'; import { MethodBadge } from '@/components/api/method-badge'; +import { useApiOperation } from '@/lib/use-api-operation'; import { ClientThemeSwitcher } from '@/components/ui/client-theme-switcher'; import { Search } from '@/components/ui/search'; import { Breadcrumbs } from '@/components/ui/breadcrumbs'; @@ -190,7 +192,10 @@ export function Layout({
{!isApiRoute && }
- + + {isApiRoute && } + +
{children} @@ -262,3 +267,24 @@ function SidebarNode({ ); } + +function ViewDocsButton() { + const match = useApiOperation(); + if (!match) return null; + + const operation = match.operation as OpenAPIV3.OperationObject; + const docsUrl = operation.externalDocs?.url ?? match.spec.document.externalDocs?.url; + if (!docsUrl) return null; + + return ( + + ); +} From 9512309076bafb6bba22a29413bcc0734e764fe6 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 12 May 2026 09:47:12 +0530 Subject: [PATCH 04/15] feat: add playground dialog with auth type switching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Test request button in navbar opens playground dialog - Split panel: editable fields left, response right - Auth type selector: API Key, Bearer Token, Basic Auth from OpenAPI securitySchemes, fallback to chronicle.yaml auth config - Preserve securityDefinitions in Swagger 2.0 → OpenAPI 3.0 conversion - Send via /api/apis-proxy, shows status + response time - Bottom bar with method badge, path, copy curl, Send button - Dedup auth header from operation header parameters Co-Authored-By: Claude Opus 4.6 (1M context) --- .../chronicle/src/components/api-v2/index.ts | 1 + .../api-v2/playground-dialog.module.css | 284 +++++++++++ .../components/api-v2/playground-dialog.tsx | 448 ++++++++++++++++++ packages/chronicle/src/lib/openapi.ts | 18 + .../chronicle/src/themes/default/Layout.tsx | 36 +- 5 files changed, 786 insertions(+), 1 deletion(-) create mode 100644 packages/chronicle/src/components/api-v2/playground-dialog.module.css create mode 100644 packages/chronicle/src/components/api-v2/playground-dialog.tsx diff --git a/packages/chronicle/src/components/api-v2/index.ts b/packages/chronicle/src/components/api-v2/index.ts index 170743b1..40dc9eff 100644 --- a/packages/chronicle/src/components/api-v2/index.ts +++ b/packages/chronicle/src/components/api-v2/index.ts @@ -2,3 +2,4 @@ export { ApiOverview } from './api-overview' export { ApiFieldSection } from './api-field-list' export { ApiCodeSnippet } from './api-code-snippet' export { ApiResponsePanel } from './api-response-panel' +export { PlaygroundDialog } from './playground-dialog' diff --git a/packages/chronicle/src/components/api-v2/playground-dialog.module.css b/packages/chronicle/src/components/api-v2/playground-dialog.module.css new file mode 100644 index 00000000..452f7481 --- /dev/null +++ b/packages/chronicle/src/components/api-v2/playground-dialog.module.css @@ -0,0 +1,284 @@ +.dialog { + padding: 0 !important; + border: 1px solid var(--rs-color-border-base-primary); + border-radius: var(--rs-radius-2); + overflow: hidden; + height: 75vh; + max-height: 700px; + width: 75vw; + max-width: 1200px; + display: flex; + flex-direction: column; +} + +.actionNav { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--rs-space-3) var(--rs-space-5); + background: var(--rs-color-background-base-secondary); + border-bottom: 0.5px solid var(--rs-color-border-base-primary); + flex-shrink: 0; + z-index: 3; +} + +.actionNavTitle { + font-size: var(--rs-font-size-small); + font-weight: var(--rs-font-weight-medium); + line-height: var(--rs-line-height-small); + color: var(--rs-color-foreground-base-primary); +} + +.splitPanel { + display: flex; + flex: 1; + min-height: 0; + overflow: hidden; + z-index: 2; +} + +.leftPanel { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; + overflow: hidden; +} + +.rightPanel { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; + border-left: 0.5px solid var(--rs-color-border-base-primary); + overflow: hidden; +} + +.panelHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--rs-space-5); + height: 42px; + border-bottom: 0.5px solid var(--rs-color-border-base-primary); + flex-shrink: 0; +} + +.panelTitle { + font-size: var(--rs-font-size-small); + font-weight: var(--rs-font-weight-medium); + line-height: var(--rs-line-height-small); + color: var(--rs-color-foreground-base-primary); + flex: 1; +} + +.tabBar { + display: flex; + align-items: center; + gap: var(--rs-space-6); + padding: var(--rs-space-2) var(--rs-space-5) 0; + border-bottom: 0.5px solid var(--rs-color-border-base-primary); + background: var(--rs-color-background-base-primary); + flex-shrink: 0; +} + +.tab { + display: flex; + align-items: center; + justify-content: center; + height: 24px; + padding: 0; + border: none; + border-bottom: 1px solid transparent; + background: transparent; + cursor: pointer; + font-size: var(--rs-font-size-small); + font-weight: var(--rs-font-weight-medium); + line-height: var(--rs-line-height-small); + letter-spacing: var(--rs-letter-spacing-small); + color: var(--rs-color-foreground-base-secondary); +} + +.tab:hover { + color: var(--rs-color-foreground-base-primary); +} + +.tabActive { + color: var(--rs-color-foreground-base-primary); + border-bottom-color: var(--rs-color-border-base-emphasis); +} + +.fieldsScroll { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; +} + +.sectionHeader { + display: flex; + align-items: center; + gap: var(--rs-space-3); + padding: var(--rs-space-3) var(--rs-space-5); + background: var(--rs-color-background-base-secondary); + border-bottom: 0.5px solid var(--rs-color-border-base-primary); + border-top: 0.5px solid var(--rs-color-border-base-primary); + flex-shrink: 0; +} + +.sectionLabel { + flex: 1; + font-size: var(--rs-font-size-mini); + font-weight: var(--rs-font-weight-medium); + line-height: var(--rs-line-height-mini); + letter-spacing: var(--rs-letter-spacing-mini); + color: var(--rs-color-foreground-base-primary); +} + +.fieldRow { + display: flex; + align-items: center; + gap: 10px; + padding: var(--rs-space-4) var(--rs-space-5); +} + +.fieldLabel { + flex: 1; + font-size: var(--rs-font-size-mini); + font-weight: var(--rs-font-weight-medium); + line-height: var(--rs-line-height-mini); + letter-spacing: var(--rs-letter-spacing-mini); + color: var(--rs-color-foreground-base-primary); + min-width: 0; +} + +.fieldInput { + width: 50%; + flex-shrink: 0; +} + +.fieldInput input { + height: 24px; + font-size: var(--rs-font-size-small); +} + +.responseHeader { + display: flex; + align-items: center; + justify-content: space-between; + padding: var(--rs-space-5); + height: 42px; + border-bottom: 0.5px solid var(--rs-color-border-base-primary); + flex-shrink: 0; +} + +.statusBar { + display: flex; + align-items: center; + gap: var(--rs-space-3); + padding: var(--rs-space-3) var(--rs-space-5); + background: var(--rs-color-background-base-secondary); + flex-shrink: 0; +} + +.statusText { + font-size: var(--rs-font-size-mini); + font-weight: var(--rs-font-weight-medium); + line-height: var(--rs-line-height-mini); + letter-spacing: var(--rs-letter-spacing-mini); + color: var(--rs-color-foreground-base-primary); +} + +.statusValue { + color: #30a46c; +} + +.statusSeparator { + width: 1px; + height: 12px; + background: var(--rs-color-border-base-tertiary); + border-radius: 2px; +} + +.responseBody { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + padding: var(--rs-space-5); +} + +.responseCode { + font-family: var(--rs-font-mono); + font-size: var(--rs-font-size-mono-small); + line-height: 18px; + color: var(--rs-color-foreground-danger-primary); + white-space: pre-wrap; + word-break: break-all; + margin: 0; +} + +.lineNumbers { + font-family: var(--rs-font-mono); + font-size: var(--rs-font-size-mono-small); + line-height: 18px; + color: var(--rs-color-foreground-base-tertiary); + opacity: 0.5; + text-align: right; + white-space: nowrap; + user-select: none; + flex-shrink: 0; +} + +.codeArea { + display: flex; + gap: var(--rs-space-5); + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + padding: var(--rs-space-5); +} + +.emptyResponse { + display: flex; + align-items: center; + justify-content: center; + flex: 1; + color: var(--rs-color-foreground-base-tertiary); + font-size: var(--rs-font-size-small); +} + +.bottomBar { + display: flex; + align-items: center; + gap: var(--rs-space-6); + padding: var(--rs-space-3) var(--rs-space-5); + background: var(--rs-color-background-base-primary); + border-top: 0.5px solid var(--rs-color-border-base-primary); + flex-shrink: 0; + z-index: 1; +} + +.pathBar { + display: flex; + align-items: center; + justify-content: space-between; + flex: 1; + min-width: 0; + padding: var(--rs-space-2); + border: 0.5px solid var(--rs-color-border-base-primary); + border-radius: var(--rs-radius-2); + background: var(--rs-color-background-base-primary); + gap: 8px; +} + +.pathText { + font-family: var(--rs-font-mono); + font-size: var(--rs-font-size-mono-small); + line-height: var(--rs-line-height-small); + color: var(--rs-color-foreground-base-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/packages/chronicle/src/components/api-v2/playground-dialog.tsx b/packages/chronicle/src/components/api-v2/playground-dialog.tsx new file mode 100644 index 00000000..15f880f7 --- /dev/null +++ b/packages/chronicle/src/components/api-v2/playground-dialog.tsx @@ -0,0 +1,448 @@ +'use client' + +import { useState, useCallback, useMemo } from 'react' +import type { OpenAPIV3 } from 'openapi-types' +import { Dialog, Button, Badge, IconButton, InputField, CopyButton, Select } from '@raystack/apsara' +import { Cross2Icon, ChevronDownIcon, ChevronUpIcon, PlayIcon } from '@radix-ui/react-icons' +import { CounterClockwiseClockIcon, CodeIcon } from '@radix-ui/react-icons' +import { MethodBadge } from '@/components/api/method-badge' +import { flattenSchema, generateExampleJson, type SchemaField } from '@/lib/schema' +import { generateCurl } from '@/lib/snippet-generators' +import styles from './playground-dialog.module.css' + +type AuthScheme = { + name: string + type: 'apiKey' | 'bearer' | 'basic' | 'none' + headerName: string + placeholder: string +} + +function getAuthSchemes( + document: OpenAPIV3.Document, + auth?: { type: string; header: string; placeholder?: string } +): AuthScheme[] { + const schemes: AuthScheme[] = [{ name: 'None', type: 'none', headerName: '', placeholder: '' }] + const securitySchemes = (document.components?.securitySchemes ?? {}) as Record + + for (const [name, scheme] of Object.entries(securitySchemes)) { + if (scheme.type === 'apiKey' && 'name' in scheme && 'in' in scheme && scheme.in === 'header') { + schemes.push({ name: `API Key (${scheme.name})`, type: 'apiKey', headerName: scheme.name!, placeholder: 'Enter API key' }) + } else if (scheme.type === 'http' && 'scheme' in scheme) { + if (scheme.scheme === 'bearer') { + schemes.push({ name: 'Bearer Token', type: 'bearer', headerName: 'Authorization', placeholder: 'Enter bearer token' }) + } else if (scheme.scheme === 'basic') { + schemes.push({ name: 'Basic Auth', type: 'basic', headerName: 'Authorization', placeholder: '' }) + } + } + } + + if (auth && !schemes.some((s) => s.headerName === auth.header && s.type !== 'none')) { + schemes.push({ name: `API Key (${auth.header})`, type: 'apiKey', headerName: auth.header, placeholder: auth.placeholder ?? 'Enter API key' }) + } + + return schemes +} + +interface PlaygroundDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + method: string + path: string + operation: OpenAPIV3.OperationObject + serverUrl: string + specName: string + auth?: { type: string; header: string; placeholder?: string } + document: OpenAPIV3.Document +} + +export function PlaygroundDialog({ + open, onOpenChange, method, path, operation, serverUrl, specName, auth, document, +}: PlaygroundDialogProps) { + const params = (operation.parameters ?? []) as OpenAPIV3.ParameterObject[] + const body = getRequestBody(operation.requestBody as OpenAPIV3.RequestBodyObject | undefined) + + const headerFields = paramsToFields(params.filter((p) => p.in === 'header')) + const pathFields = paramsToFields(params.filter((p) => p.in === 'path')) + const queryFields = paramsToFields(params.filter((p) => p.in === 'query')) + + const authSchemes = useMemo(() => getAuthSchemes(document, auth), [document, auth]) + const defaultScheme = authSchemes.find((s) => s.type !== 'none') ?? authSchemes[0] + + const [selectedScheme, setSelectedScheme] = useState(defaultScheme.name) + const [authToken, setAuthToken] = useState('') + const [basicUser, setBasicUser] = useState('') + const [basicPass, setBasicPass] = useState('') + const [headerValues, setHeaderValues] = useState>({}) + const [pathValues, setPathValues] = useState>({}) + const [queryValues, setQueryValues] = useState>({}) + const [bodyValues, setBodyValues] = useState>(() => { + if (!body) return {} + const init: Record = {} + for (const f of body.fields) init[f.name] = '' + return init + }) + + const [responseData, setResponseData] = useState<{ + status: number; statusText: string; body: unknown; time: number + } | null>(null) + const [loading, setLoading] = useState(false) + const [collapsed, setCollapsed] = useState>({}) + + const toggleCollapse = (section: string) => { + setCollapsed((prev) => ({ ...prev, [section]: !prev[section] })) + } + + const currentScheme = authSchemes.find((s) => s.name === selectedScheme) ?? authSchemes[0] + + const getAuthHeaders = useCallback((): Record => { + const headers: Record = {} + if (currentScheme.type === 'apiKey' && authToken) { + headers[currentScheme.headerName] = authToken + } else if (currentScheme.type === 'bearer' && authToken) { + headers['Authorization'] = `Bearer ${authToken}` + } else if (currentScheme.type === 'basic' && (basicUser || basicPass)) { + headers['Authorization'] = `Basic ${btoa(`${basicUser}:${basicPass}`)}` + } + return headers + }, [currentScheme, authToken, basicUser, basicPass]) + + const handleReset = () => { + setSelectedScheme(defaultScheme.name) + setAuthToken('') + setBasicUser('') + setBasicPass('') + setHeaderValues({}) + setPathValues({}) + setQueryValues({}) + setBodyValues(() => { + if (!body) return {} + const init: Record = {} + for (const f of body.fields) init[f.name] = '' + return init + }) + setResponseData(null) + } + + const handleSend = useCallback(async () => { + setLoading(true) + setResponseData(null) + const startTime = performance.now() + + let resolvedPath = path + for (const [key, value] of Object.entries(pathValues)) { + if (value) resolvedPath = resolvedPath.replace(`{${key}}`, encodeURIComponent(value)) + } + + const queryEntries = Object.entries(queryValues).filter(([, v]) => v) + const queryString = queryEntries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&') + const fullPath = queryString ? `${resolvedPath}?${queryString}` : resolvedPath + + const reqHeaders: Record = { ...getAuthHeaders() } + for (const [key, value] of Object.entries(headerValues)) { + if (value) reqHeaders[key] = value + } + + let reqBody: unknown = undefined + if (body) { + reqHeaders['Content-Type'] = body.contentType ?? 'application/json' + const parsed: Record = {} + for (const [k, v] of Object.entries(bodyValues)) { + if (v) parsed[k] = v + } + reqBody = parsed + } + + try { + const res = await fetch('/api/apis-proxy', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ specName, method, path: fullPath, headers: reqHeaders, body: reqBody }), + }) + const data = await res.json() + const elapsed = Math.round(performance.now() - startTime) + if (data.status !== undefined) { + setResponseData({ ...data, time: elapsed }) + } else { + setResponseData({ status: res.status, statusText: res.statusText, body: data.error ?? data, time: elapsed }) + } + } catch { + setResponseData({ status: 0, statusText: 'Error', body: 'Failed to send request', time: 0 }) + } finally { + setLoading(false) + } + }, [specName, method, path, pathValues, queryValues, getAuthHeaders, headerValues, bodyValues, body]) + + const responseJson = responseData + ? (typeof responseData.body === 'string' ? responseData.body : JSON.stringify(responseData.body, null, 2)) + : '' + + const responseLines = responseJson ? responseJson.split('\n') : [] + + const curlSnippet = useMemo(() => { + const headers: Record = { ...getAuthHeaders(), ...headerValues } + if (body) headers['Content-Type'] = body.contentType ?? 'application/json' + return generateCurl({ method, url: serverUrl + path, headers, body: body ? JSON.stringify(bodyValues) : undefined }) + }, [method, path, serverUrl, getAuthHeaders, headerValues, bodyValues, body]) + + + return ( + + + {/* Action Nav */} +
+ {operation.summary ?? `${method} ${path}`} +
+ + + + onOpenChange(false)} aria-label="Close"> + + +
+
+ + {/* Split Panel */} +
+ {/* Left Panel */} +
+
+ Test request +
+ + {/* Fields */} +
+ {/* Auth Section */} + {( + <> +
+ Authorization + toggleCollapse('auth')}> + {collapsed.auth ? : } + +
+ {!collapsed.auth && ( + <> + {authSchemes.length > 2 && ( +
+ Type +
+ +
+
+ )} + {currentScheme.type === 'basic' ? ( + <> +
+ Username +
+ setBasicUser(e.target.value)} /> +
+
+
+ Password +
+ setBasicPass(e.target.value)} /> +
+
+ + ) : currentScheme.type !== 'none' ? ( +
+ {currentScheme.headerName} +
+ setAuthToken(e.target.value)} /> +
+
+ ) : null} + {headerFields.filter((f) => f.name !== currentScheme.headerName).map((f) => ( +
+ {f.name} +
+ setHeaderValues({ ...headerValues, [f.name]: e.target.value })} /> +
+
+ ))} + + )} + + )} + + {/* Path Params */} + {pathFields.length > 0 && ( + <> +
+ Path Parameters + toggleCollapse('path')}> + {collapsed.path ? : } + +
+ {!collapsed.path && pathFields.map((f) => ( +
+ {f.name} +
+ setPathValues({ ...pathValues, [f.name]: e.target.value })} + /> +
+
+ ))} + + )} + + {/* Query Params */} + {queryFields.length > 0 && ( + <> +
+ Query Parameters + toggleCollapse('query')}> + {collapsed.query ? : } + +
+ {!collapsed.query && queryFields.map((f) => ( +
+ {f.name} +
+ setQueryValues({ ...queryValues, [f.name]: e.target.value })} + /> +
+
+ ))} + + )} + + {/* Body Section */} + {body && ( + <> +
+ Body +
+ + + + toggleCollapse('body')}> + {collapsed.body ? : } + +
+
+ {!collapsed.body && body.fields.map((f) => ( +
+ {f.name} +
+ setBodyValues({ ...bodyValues, [f.name]: e.target.value })} + /> +
+
+ ))} + + )} +
+
+ + {/* Right Panel */} +
+
+ Response +
+ + {responseData ? ( + <> +
+ + Status : {responseData.status} + +
+ + Time : {responseData.time} ms + +
+
+
+ {responseLines.map((_, i) => ( +
{i + 1}
+ ))} +
+
{responseJson}
+
+ + ) : ( +
+ {loading ? 'Sending...' : 'Send a request to see the response'} +
+ )} +
+
+ + {/* Bottom Bar */} +
+
+
+ + {path} +
+ +
+ +
+ +
+ ) +} + +function paramsToFields(params: OpenAPIV3.ParameterObject[]): SchemaField[] { + return params.map((p) => { + const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject + return { + name: p.name, + type: schema.type ? String(schema.type) : 'string', + required: p.required ?? false, + description: p.description, + } + }) +} + +interface RequestBody { + contentType: string + fields: SchemaField[] + jsonExample: string +} + +function getRequestBody(body: OpenAPIV3.RequestBodyObject | undefined): RequestBody | null { + if (!body?.content) return null + const contentType = Object.keys(body.content)[0] + if (!contentType) return null + const schema = body.content[contentType]?.schema as OpenAPIV3.SchemaObject | undefined + if (!schema) return null + return { + contentType, + fields: flattenSchema(schema), + jsonExample: JSON.stringify(generateExampleJson(schema), null, 2), + } +} diff --git a/packages/chronicle/src/lib/openapi.ts b/packages/chronicle/src/lib/openapi.ts index 6e12e78b..379c84a3 100644 --- a/packages/chronicle/src/lib/openapi.ts +++ b/packages/chronicle/src/lib/openapi.ts @@ -120,13 +120,31 @@ function convertV2toV3(doc: OpenAPIV2.Document): OpenAPIV3.Document { v3Paths[pathStr] = v3PathItem } + const securitySchemes = convertV2SecurityDefs(resolved.securityDefinitions as Record | undefined) + return { openapi: '3.0.0', info: resolved.info as unknown as OpenAPIV3.InfoObject, paths: v3Paths, tags: (resolved.tags ?? []) as unknown as OpenAPIV3.TagObject[], ...(resolved.externalDocs ? { externalDocs: resolved.externalDocs as unknown as OpenAPIV3.ExternalDocumentationObject } : {}), + ...(Object.keys(securitySchemes).length > 0 ? { components: { securitySchemes } } : {}), + } +} + +function convertV2SecurityDefs(defs: Record | undefined): Record { + if (!defs) return {} + const result: Record = {} + for (const [name, def] of Object.entries(defs)) { + if (def.type === 'apiKey') { + result[name] = { type: 'apiKey', name: (def as JsonObject).name as string, in: def.in as string } as OpenAPIV3.ApiKeySecurityScheme + } else if (def.type === 'basic') { + result[name] = { type: 'http', scheme: 'basic' } as OpenAPIV3.HttpSecurityScheme + } else if (def.type === 'oauth2') { + result[name] = { type: 'oauth2', flows: {} } as OpenAPIV3.OAuth2SecurityScheme + } } + return result } function convertV2Operation(op: OpenAPIV2.OperationObject): OpenAPIV3.OperationObject { diff --git a/packages/chronicle/src/themes/default/Layout.tsx b/packages/chronicle/src/themes/default/Layout.tsx index dcc2a6ae..f2ae512a 100644 --- a/packages/chronicle/src/themes/default/Layout.tsx +++ b/packages/chronicle/src/themes/default/Layout.tsx @@ -7,12 +7,14 @@ import { Squares2X2Icon } from '@heroicons/react/24/outline'; import { Flex, IconButton, Button, Sidebar } from '@raystack/apsara'; +import { PlayIcon } from '@radix-ui/react-icons'; import { cx } from 'class-variance-authority'; -import { useEffect, useMemo, useRef } from 'react'; +import { useState, useEffect, useMemo, useRef } from 'react'; import { Link as RouterLink, useLocation, useNavigate } from 'react-router'; import type { OpenAPIV3 } from 'openapi-types'; import { MethodBadge } from '@/components/api/method-badge'; import { useApiOperation } from '@/lib/use-api-operation'; +import { PlaygroundDialog } from '@/components/api-v2/playground-dialog'; import { ClientThemeSwitcher } from '@/components/ui/client-theme-switcher'; import { Search } from '@/components/ui/search'; import { Breadcrumbs } from '@/components/ui/breadcrumbs'; @@ -193,6 +195,7 @@ export function Layout({ {!isApiRoute && } + {isApiRoute && } {isApiRoute && } @@ -268,6 +271,37 @@ function SidebarNode({ ); } +function TestRequestButton() { + const match = useApiOperation(); + const [open, setOpen] = useState(false); + if (!match) return null; + + return ( + <> + + + + ); +} + function ViewDocsButton() { const match = useApiOperation(); if (!match) return null; From b168258f4379cf290beed0b07f96141f93335ebc Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 12 May 2026 09:47:51 +0530 Subject: [PATCH 05/15] chore: remove api-v2 PLAN.md Co-Authored-By: Claude Opus 4.6 (1M context) --- .../chronicle/src/components/api-v2/PLAN.md | 220 ------------------ 1 file changed, 220 deletions(-) delete mode 100644 packages/chronicle/src/components/api-v2/PLAN.md diff --git a/packages/chronicle/src/components/api-v2/PLAN.md b/packages/chronicle/src/components/api-v2/PLAN.md deleted file mode 100644 index 68a70c0d..00000000 --- a/packages/chronicle/src/components/api-v2/PLAN.md +++ /dev/null @@ -1,220 +0,0 @@ -# API Reference Page Redesign — TODO - -## Context - -Current `EndpointPage` combines read-only docs + editable "try it out" in one view. Redesign separates: -- **Overview page**: read-only, field names + types only -- **Playground dialog**: opens via navbar "Test request" button, all editable fields + live response -- **Navbar**: 3 action buttons (Test request, View documentation, Open in ChatGPT) - -Build all new components from scratch in `components/api-v2/`. Old components stay untouched. - ---- - -## Phase 1: Read-only API Overview - -### TODO -- [ ] Create `api-overview.tsx` + `api-overview.module.css` -- [ ] Create `api-field-list.tsx` + `api-field-list.module.css` - -### `ApiOverview` — main page component - -Two-column grid layout: - -**Left column:** -- Title (h1, `--rs-font-size-t3`) -- Description (secondary color text) -- Method badge + path + copy icon (no Send button) -- **Authorisations** section: `Badge(name)` + "String" type text -- **Query Parameters** section: per field `Badge(name)` + type + description, bottom border separator -- **Response** section: "Response" header + "application/json" + status dropdown (200), description ("OK"), fields same format, "Show child attributes" expandable for nested objects - -**Right column:** -- Code snippet (cURL, Python, Go, TS) with language dropdown + copy -- Response JSON with status code tabs (200, 400, 404, 500) + copy - -### `ApiFieldList` — read-only field display - -Each field renders: -``` -[Badge: field_name] type_text -description (optional) -─── border-bottom ─── -``` - -Nested objects: "Show child attributes" expandable row (bg secondary, border, chevron icon). - -### Reuse (import): -- `MethodBadge` from `components/api/method-badge` -- `flattenSchema`, `generateExampleJson`, `SchemaField` from `lib/schema` - -### Copy (internalize): -- `paramsToFields`, `getRequestBody`, `getResponseSections` helper functions from `endpoint-page.tsx` - ---- - -## Phase 2: Playground Dialog - -### TODO -- [ ] Create `playground-dialog.tsx` + `playground-dialog.module.css` -- [ ] Create `playground-field-row.tsx` + `playground-field-row.module.css` - -### `PlaygroundDialog` — full editable playground - -Uses Apsara `Dialog` (`open`/`onOpenChange` controlled, ~900px width). - -**Structure:** -``` -┌─ Action Nav ──────────────────────────────────────┐ -│ [Breadcrumb: endpoint name] [Reset] [Close] │ -├─ Split panel (flex 1:1) ──────────────────────────┤ -│ Left Panel │ Right Panel │ -│ │ │ -│ "Test request" [JSON]│ "Response" [Body ▾] │ -│ [All] [Auth] [Body] │ Status:200 | Time:987ms │ -│ │ [Curl ▾] │ -│ ┌Authorization ▾┐ │ │ -│ │ Authorization │ │ (code snippet w/ line nos) │ -│ │ [input field] │ │ │ -│ ├Body ▾┤ │ │ -│ │ Name [input] │ │ │ -│ │ Description ... │ │ │ -│ │ Nested arrays │ │ │ -│ └─────────────────┘ │ │ -├─ Bottom bar ──────────────────────────────────────┤ -│ [POST badge] /v0/projects [copy] [Send ▶] │ -└───────────────────────────────────────────────────┘ -``` - -**Action Nav:** -- Breadcrumb showing endpoint name (left) -- Reset icon button + Close (X) icon button (right) - -**Left panel:** -- Tab bar: All | Auth | Body — plain underline tabs (active = border-bottom emphasis, not Apsara Tabs) -- Collapsible sections: gray bg header row (label + chevron), content below -- **Authorization section**: label "Authorization" + `InputField` (24px height, placeholder "Enter ID") -- **Body section**: header has label + JSON toggle (`` icon) + chevron - - Each body field: label (11px medium) left, `InputField` right (168px width) - - Nested arrays: dashed left border, indented children, add (+) / remove (X) / collapse (chevron) icons - -**Right panel:** -- "Response" header + Body dropdown button -- Status bar (gray bg): `Status: {code}` (green) | separator | `Time: {ms}` (green) | Curl dropdown -- Code snippet: line numbers (gray, right-aligned) + response body (monospace, red/syntax colored) - -**Bottom bar:** -- Rounded bordered container: Badge (method) + monospace path + copy icon -- "Send" button: `variant="solid" color="accent" size="small" trailingIcon={PlayIcon}` - -**State** (all `useState` inside dialog): -- `customHeaders`, `headerValues`, `pathValues`, `queryValues` -- `bodyValues`, `bodyJsonStr` (two-way sync) -- `responseBody`, `loading`, `responseTime` -- `activeTab` (All/Auth/Body) -- `collapsedSections` (Set of section names) -- `jsonView` (boolean for body JSON toggle) - -**Send handler:** POST to `/api/apis-proxy` with `{ specName, method, path, headers, body }`. Track response time. - -### `PlaygroundFieldRow` — editable field row - -Layout: `label (flex-1) | InputField (168px)` -- 11px medium font for label -- InputField 24px height, 12px font, placeholder text -- For nested: dashed left border, indented, X button to remove - ---- - -## Phase 3: Navbar Action Buttons - -### TODO -- [ ] Create `api-nav-actions.tsx` + `api-nav-actions.module.css` - -### `ApiNavActions` - -Renders 3 buttons + hosts `PlaygroundDialog`: - -1. **Test request** — `Button variant="outline" color="neutral" size="small" leadingIcon={PlayIcon}` → opens playground dialog -2. **View documentation** — `Button variant="outline" color="neutral" size="small" leadingIcon={DocumentIcon}` → navigates to docs page -3. **Open in ChatGPT** — `Button variant="outline" color="neutral" size="small" leadingIcon={ChatGPTIcon} trailingIcon={ChevronDownIcon}` → dropdown menu with: Copy as MD, View MD, Open in ChatGPT, Open in Claude (same as existing `OpenInAI`) - -Reads `apiOperation` from page context to pass to `PlaygroundDialog`. - -Manages `playgroundOpen` state locally. - ---- - -## Phase 4: Wire Together - -### TODO -- [ ] Update `lib/page-context.tsx` — add `apiOperation` field -- [ ] Update `pages/ApiPage.tsx` — import `ApiOverview` instead of `EndpointPage`, set `apiOperation` in context -- [ ] Update `themes/default/Layout.tsx` — render `ApiNavActions` on API endpoint pages (when `apiOperation` exists in context) -- [ ] Create `components/api-v2/index.ts` — barrel exports - -### `page-context.tsx` changes - -Add to context type: -```ts -apiOperation?: { - method: string - path: string - operation: OpenAPIV3.OperationObject - serverUrl: string - specName: string - auth?: { type: string; header: string; placeholder?: string } -} -``` - -### `ApiPage.tsx` changes - -```tsx -// Before: -// After: set apiOperation in context + -``` - -### `Layout.tsx` changes - -```tsx -// Line 193 area: -{apiOperation ? : } -``` - ---- - -## File Summary - -| File | Action | -|------|--------| -| `components/api-v2/api-overview.tsx` | **NEW** | -| `components/api-v2/api-overview.module.css` | **NEW** | -| `components/api-v2/api-field-list.tsx` | **NEW** | -| `components/api-v2/api-field-list.module.css` | **NEW** | -| `components/api-v2/playground-dialog.tsx` | **NEW** | -| `components/api-v2/playground-dialog.module.css` | **NEW** | -| `components/api-v2/playground-field-row.tsx` | **NEW** | -| `components/api-v2/playground-field-row.module.css` | **NEW** | -| `components/api-v2/api-nav-actions.tsx` | **NEW** | -| `components/api-v2/api-nav-actions.module.css` | **NEW** | -| `components/api-v2/index.ts` | **NEW** | -| `lib/page-context.tsx` | Modify — add `apiOperation` | -| `pages/ApiPage.tsx` | Modify — use new components | -| `themes/default/Layout.tsx` | Modify — conditional navbar | - -**Untouched**: all existing `components/api/` files. - ---- - -## Verification - -1. `bun run build:cli` -2. `bun run dev:examples:basic` → API reference pages -3. Overview page: read-only (Badge + type), two-column, no inputs -4. Navbar: 3 buttons on API endpoint pages -5. "Test request" → dialog opens with editable fields -6. Fill + Send → response shows (status, time, JSON) -7. Close → back to overview -8. "View documentation" + "Open in ChatGPT" work -9. Non-API pages: `` only -10. `bunx tsc --noEmit --project packages/chronicle/tsconfig.json` passes From 53f2a6ead06d65994e260968f50a87c82957d451 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 12 May 2026 15:08:52 +0530 Subject: [PATCH 06/15] feat: improve playground fields and schema handling - Add kind field to SchemaField for reliable type detection - Resolve allOf/oneOf/anyOf in flattenSchema for nested objects - Auto-generate operationId for endpoints missing it (method + path) - Array fields: add/remove UI for primitive arrays (string[], number[]) - Object fields: render child fields recursively with indented layout - Reset dialog state on endpoint change via React key - Field list gap and array item alignment fixes Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/api-v2/api-field-list.tsx | 2 +- .../src/components/api-v2/api-overview.tsx | 3 +- .../api-v2/playground-dialog.module.css | 28 +++- .../components/api-v2/playground-dialog.tsx | 120 ++++++++++++++---- packages/chronicle/src/lib/api-routes.ts | 28 ++-- packages/chronicle/src/lib/schema.ts | 36 +++++- .../chronicle/src/themes/default/Layout.tsx | 1 + 7 files changed, 179 insertions(+), 39 deletions(-) diff --git a/packages/chronicle/src/components/api-v2/api-field-list.tsx b/packages/chronicle/src/components/api-v2/api-field-list.tsx index 4121f0e0..6df922d4 100644 --- a/packages/chronicle/src/components/api-v2/api-field-list.tsx +++ b/packages/chronicle/src/components/api-v2/api-field-list.tsx @@ -27,7 +27,7 @@ export function ApiFieldSection({ title, fields, headerRight, description }: Api )} {description && {description}} - + {fields.map((field) => ( ))} diff --git a/packages/chronicle/src/components/api-v2/api-overview.tsx b/packages/chronicle/src/components/api-v2/api-overview.tsx index d0965b11..6b00b508 100644 --- a/packages/chronicle/src/components/api-v2/api-overview.tsx +++ b/packages/chronicle/src/components/api-v2/api-overview.tsx @@ -30,7 +30,7 @@ export function ApiOverview({ method, path, operation, auth }: ApiOverviewProps) const responses = getResponseSections(operation.responses as Record) const authFields: SchemaField[] = auth - ? [{ name: auth.header, type: 'String', required: false }] + ? [{ name: auth.header, type: 'String', kind: 'string' as const, required: false }] : headerFields.length > 0 ? headerFields : [] @@ -161,6 +161,7 @@ function paramsToFields(params: OpenAPIV3.ParameterObject[]): SchemaField[] { return { name: p.name, type: schema.type ? String(schema.type) : 'string', + kind: (schema.type as SchemaField['kind']) ?? 'string', required: p.required ?? false, description: p.description, default: schema.default, diff --git a/packages/chronicle/src/components/api-v2/playground-dialog.module.css b/packages/chronicle/src/components/api-v2/playground-dialog.module.css index 452f7481..533bf4f5 100644 --- a/packages/chronicle/src/components/api-v2/playground-dialog.module.css +++ b/packages/chronicle/src/components/api-v2/playground-dialog.module.css @@ -3,10 +3,10 @@ border: 1px solid var(--rs-color-border-base-primary); border-radius: var(--rs-radius-2); overflow: hidden; - height: 75vh; - max-height: 700px; - width: 75vw; - max-width: 1200px; + height: 70vh; + max-height: 600px; + width: 70vw; + max-width: 900px; display: flex; flex-direction: column; } @@ -113,6 +113,7 @@ min-height: 0; overflow-y: auto; overflow-x: hidden; + padding-bottom: var(--rs-space-5); } .sectionHeader { @@ -162,6 +163,25 @@ font-size: var(--rs-font-size-small); } +.arrayField { + display: flex; + flex-direction: column; +} + +.nestedFields { + padding-left: var(--rs-space-5); + border-left: 1px dashed var(--rs-color-border-base-primary); + margin-left: var(--rs-space-5); +} + +.arrayItemRow { + display: flex; + align-items: center; + justify-content: flex-end; + gap: var(--rs-space-3); + padding: var(--rs-space-2) var(--rs-space-5) var(--rs-space-2) var(--rs-space-8); +} + .responseHeader { display: flex; align-items: center; diff --git a/packages/chronicle/src/components/api-v2/playground-dialog.tsx b/packages/chronicle/src/components/api-v2/playground-dialog.tsx index 15f880f7..c3c55815 100644 --- a/packages/chronicle/src/components/api-v2/playground-dialog.tsx +++ b/packages/chronicle/src/components/api-v2/playground-dialog.tsx @@ -3,7 +3,7 @@ import { useState, useCallback, useMemo } from 'react' import type { OpenAPIV3 } from 'openapi-types' import { Dialog, Button, Badge, IconButton, InputField, CopyButton, Select } from '@raystack/apsara' -import { Cross2Icon, ChevronDownIcon, ChevronUpIcon, PlayIcon } from '@radix-ui/react-icons' +import { Cross2Icon, ChevronDownIcon, ChevronUpIcon, PlayIcon, PlusIcon } from '@radix-ui/react-icons' import { CounterClockwiseClockIcon, CodeIcon } from '@radix-ui/react-icons' import { MethodBadge } from '@/components/api/method-badge' import { flattenSchema, generateExampleJson, type SchemaField } from '@/lib/schema' @@ -75,10 +75,14 @@ export function PlaygroundDialog({ const [headerValues, setHeaderValues] = useState>({}) const [pathValues, setPathValues] = useState>({}) const [queryValues, setQueryValues] = useState>({}) - const [bodyValues, setBodyValues] = useState>(() => { + const [bodyValues, setBodyValues] = useState>(() => { if (!body) return {} - const init: Record = {} - for (const f of body.fields) init[f.name] = '' + const init: Record = {} + for (const f of body.fields) { + if (f.kind === 'array') init[f.name] = [] + else if (f.kind === 'object' || f.children) init[f.name] = {} + else init[f.name] = '' + } return init }) @@ -116,8 +120,12 @@ export function PlaygroundDialog({ setQueryValues({}) setBodyValues(() => { if (!body) return {} - const init: Record = {} - for (const f of body.fields) init[f.name] = '' + const init: Record = {} + for (const f of body.fields) { + if (f.type.endsWith('[]')) init[f.name] = [] + else if (f.children) init[f.name] = {} + else init[f.name] = '' + } return init }) setResponseData(null) @@ -145,11 +153,7 @@ export function PlaygroundDialog({ let reqBody: unknown = undefined if (body) { reqHeaders['Content-Type'] = body.contentType ?? 'application/json' - const parsed: Record = {} - for (const [k, v] of Object.entries(bodyValues)) { - if (v) parsed[k] = v - } - reqBody = parsed + reqBody = bodyValues } try { @@ -340,17 +344,12 @@ export function PlaygroundDialog({ {!collapsed.body && body.fields.map((f) => ( -
- {f.name} -
- setBodyValues({ ...bodyValues, [f.name]: e.target.value })} - /> -
-
+ setBodyValues({ ...bodyValues, [f.name]: val })} + /> ))} )} @@ -416,12 +415,89 @@ export function PlaygroundDialog({ ) } +function BodyFieldRow({ field, value, onChange }: { + field: SchemaField + value: unknown + onChange: (val: unknown) => void +}) { + const hasChildren = field.children && field.children.length > 0 + + if (field.kind === 'array' && !hasChildren) { + const items = Array.isArray(value) ? value as string[] : [] + return ( +
+
+ {field.name} + onChange([...items, ''])} aria-label="Add item"> + + +
+ {items.map((item, i) => ( +
+
+ { + const updated = [...items] + updated[i] = e.target.value + onChange(updated) + }} + /> +
+ onChange(items.filter((_, j) => j !== i))} aria-label="Remove item"> + + +
+ ))} +
+ ) + } + + if (hasChildren) { + const objValue = (typeof value === 'object' && value !== null ? value : {}) as Record + return ( +
+
+ {field.name} +
+
+ {field.children!.map((child) => ( + onChange({ ...objValue, [child.name]: val })} + /> + ))} +
+
+ ) + } + + return ( +
+ {field.name} +
+ onChange(e.target.value)} + /> +
+
+ ) +} + function paramsToFields(params: OpenAPIV3.ParameterObject[]): SchemaField[] { return params.map((p) => { const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject return { name: p.name, type: schema.type ? String(schema.type) : 'string', + kind: (schema.type as SchemaField['kind']) ?? 'string', required: p.required ?? false, description: p.description, } diff --git a/packages/chronicle/src/lib/api-routes.ts b/packages/chronicle/src/lib/api-routes.ts index ce30fc04..459aedba 100644 --- a/packages/chronicle/src/lib/api-routes.ts +++ b/packages/chronicle/src/lib/api-routes.ts @@ -7,6 +7,14 @@ export function getSpecSlug(spec: ApiSpec): string { return slugify(spec.name, { lower: true, strict: true }) } +function deriveOperationId(method: string, path: string): string { + return `${method}_${path.replace(/[/{}\-]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '')}` +} + +function getOperationId(op: OpenAPIV3.OperationObject, method: string, path: string): string { + return op.operationId || deriveOperationId(method, path) +} + export function buildApiRoutes(specs: ApiSpec[]): { slug: string[] }[] { const routes: { slug: string[] }[] = [] @@ -15,12 +23,13 @@ export function buildApiRoutes(specs: ApiSpec[]): { slug: string[] }[] { const specSlug = getSpecSlug(spec) const paths = spec.document.paths ?? {} - for (const [, pathItem] of Object.entries(paths)) { + for (const [pathStr, pathItem] of Object.entries(paths)) { if (!pathItem) continue for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) { const op = pathItem[method] - if (!op?.operationId) continue - routes.push({ slug: [specSlug, encodeURIComponent(op.operationId)] }) + if (!op) continue + const opId = getOperationId(op, method, pathStr) + routes.push({ slug: [specSlug, encodeURIComponent(opId)] }) } } } @@ -47,7 +56,9 @@ export function findApiOperation(specs: ApiSpec[], slug: string[]): ApiRouteMatc if (!pathItem) continue for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) { const op = pathItem[method] - if (op?.operationId && encodeURIComponent(op.operationId) === operationId) { + if (!op) continue + const opId = getOperationId(op, method, pathStr) + if (encodeURIComponent(opId) === operationId) { return { spec, operation: op, method: method.toUpperCase(), path: pathStr } } } @@ -67,12 +78,13 @@ export function buildApiPageTree(specs: ApiSpec[]): Root { const opsByTag = new Map() const tagDisplayName = new Map() - for (const [, pathItem] of Object.entries(paths)) { + for (const [pathStr, pathItem] of Object.entries(paths)) { if (!pathItem) continue for (const method of ['get', 'post', 'put', 'delete', 'patch'] as const) { const op = pathItem[method] - if (!op?.operationId) continue + if (!op) continue + const opId = getOperationId(op, method, pathStr) const rawTag = op.tags?.[0] ?? 'default' const tagKey = rawTag.toLowerCase() if (!opsByTag.has(tagKey)) { @@ -82,8 +94,8 @@ export function buildApiPageTree(specs: ApiSpec[]): Root { opsByTag.get(tagKey)!.push({ type: 'page', - name: op.summary ?? op.operationId, - url: `/apis/${specSlug}/${encodeURIComponent(op.operationId)}`, + name: op.summary ?? opId, + url: `/apis/${specSlug}/${encodeURIComponent(opId)}`, icon: `method-${method}`, }) } diff --git a/packages/chronicle/src/lib/schema.ts b/packages/chronicle/src/lib/schema.ts index ff846161..43dec6c5 100644 --- a/packages/chronicle/src/lib/schema.ts +++ b/packages/chronicle/src/lib/schema.ts @@ -1,8 +1,11 @@ import type { OpenAPIV3 } from 'openapi-types' +export type SchemaFieldKind = 'string' | 'integer' | 'number' | 'boolean' | 'array' | 'object' + export interface SchemaField { name: string type: string + kind: SchemaFieldKind required: boolean description?: string default?: unknown @@ -10,10 +13,33 @@ export interface SchemaField { children?: SchemaField[] } +function mergeAllOf(schema: OpenAPIV3.SchemaObject): OpenAPIV3.SchemaObject { + const composed = schema.allOf ?? schema.oneOf ?? schema.anyOf + if (!composed) return schema + const merged: OpenAPIV3.SchemaObject = { ...schema } + delete merged.allOf + delete merged.oneOf + delete merged.anyOf + for (const sub of composed as OpenAPIV3.SchemaObject[]) { + if (sub.type) merged.type = sub.type + if (sub.properties) { + merged.properties = { ...(merged.properties ?? {}), ...sub.properties } + } + if (sub.required) { + merged.required = [...(merged.required ?? []), ...sub.required] + } + if (sub.description && !merged.description) merged.description = sub.description + } + return merged +} + export function flattenSchema( schema: OpenAPIV3.SchemaObject, requiredFields: string[] = [], ): SchemaField[] { + const resolved = mergeAllOf(schema) + if (resolved !== schema) return flattenSchema(resolved, requiredFields) + if (schema.type === 'array' && schema.items) { const items = schema.items as OpenAPIV3.SchemaObject const itemType = inferType(items) @@ -26,6 +52,7 @@ export function flattenSchema( return [{ name: 'items', type: `${itemType}[]`, + kind: 'array' as SchemaFieldKind, required: true, description: items.description, children: children?.length ? children : undefined, @@ -36,7 +63,8 @@ export function flattenSchema( const properties = (schema.properties ?? {}) as Record const required = schema.required ?? requiredFields - return Object.entries(properties).map(([name, prop]) => { + return Object.entries(properties).map(([name, rawProp]) => { + const prop = mergeAllOf(rawProp) const fieldType = inferType(prop) const children = fieldType === 'object' || prop.properties @@ -48,8 +76,9 @@ export function flattenSchema( return { name, type: fieldType, + kind: (prop.type as SchemaFieldKind) ?? 'object', required: required.includes(name), - description: prop.description, + description: rawProp.description ?? prop.description, default: prop.default, enum: prop.enum, children: children?.length ? children : undefined, @@ -87,7 +116,8 @@ export function generateExampleJson(schema: OpenAPIV3.SchemaObject): unknown { return defaults[schema.type as string] ?? null } -function inferType(schema: OpenAPIV3.SchemaObject): string { +function inferType(rawSchema: OpenAPIV3.SchemaObject): string { + const schema = mergeAllOf(rawSchema) if (schema.type === 'array') { const items = schema.items as OpenAPIV3.SchemaObject | undefined const itemType = items ? inferType(items) : 'unknown' diff --git a/packages/chronicle/src/themes/default/Layout.tsx b/packages/chronicle/src/themes/default/Layout.tsx index f2ae512a..190cff7f 100644 --- a/packages/chronicle/src/themes/default/Layout.tsx +++ b/packages/chronicle/src/themes/default/Layout.tsx @@ -288,6 +288,7 @@ function TestRequestButton() { Test request Date: Tue, 12 May 2026 15:18:28 +0530 Subject: [PATCH 07/15] feat: JSON body editor and response headers in playground - Toggle between form fields and CodeMirror JSON editor via icon - Two-way sync between form and JSON views - Response panel dropdown to switch between Body and Headers view - Proxy returns response headers from upstream - Fix JsonEditor cursor reset by using ref for onChange callback Co-Authored-By: Claude Opus 4.6 (1M context) --- .../api-v2/playground-dialog.module.css | 38 ++++++++ .../components/api-v2/playground-dialog.tsx | 90 +++++++++++++++---- .../src/components/api/json-editor.tsx | 16 ++-- .../chronicle/src/server/api/apis-proxy.ts | 6 +- 4 files changed, 122 insertions(+), 28 deletions(-) diff --git a/packages/chronicle/src/components/api-v2/playground-dialog.module.css b/packages/chronicle/src/components/api-v2/playground-dialog.module.css index 533bf4f5..5214844a 100644 --- a/packages/chronicle/src/components/api-v2/playground-dialog.module.css +++ b/packages/chronicle/src/components/api-v2/playground-dialog.module.css @@ -182,6 +182,12 @@ padding: var(--rs-space-2) var(--rs-space-5) var(--rs-space-2) var(--rs-space-8); } +.jsonEditorWrap { + flex: 1; + min-height: 150px; + border-bottom: 0.5px solid var(--rs-color-border-base-primary); +} + .responseHeader { display: flex; align-items: center; @@ -260,6 +266,38 @@ padding: var(--rs-space-5); } +.headersArea { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: var(--rs-space-3) 0; +} + +.headerRow { + display: flex; + align-items: baseline; + gap: var(--rs-space-3); + padding: var(--rs-space-2) var(--rs-space-5); + border-bottom: 0.5px solid var(--rs-color-border-base-primary); +} + +.headerKey { + font-family: var(--rs-font-mono); + font-size: var(--rs-font-size-mono-small); + line-height: 18px; + color: var(--rs-color-foreground-base-primary); + font-weight: var(--rs-font-weight-medium); + white-space: nowrap; +} + +.headerValue { + font-family: var(--rs-font-mono); + font-size: var(--rs-font-size-mono-small); + line-height: 18px; + color: var(--rs-color-foreground-base-secondary); + word-break: break-all; +} + .emptyResponse { display: flex; align-items: center; diff --git a/packages/chronicle/src/components/api-v2/playground-dialog.tsx b/packages/chronicle/src/components/api-v2/playground-dialog.tsx index c3c55815..bea5dfe0 100644 --- a/packages/chronicle/src/components/api-v2/playground-dialog.tsx +++ b/packages/chronicle/src/components/api-v2/playground-dialog.tsx @@ -2,12 +2,13 @@ import { useState, useCallback, useMemo } from 'react' import type { OpenAPIV3 } from 'openapi-types' -import { Dialog, Button, Badge, IconButton, InputField, CopyButton, Select } from '@raystack/apsara' +import { Dialog, Button, Badge, IconButton, InputField, CopyButton, Select, Menu } from '@raystack/apsara' import { Cross2Icon, ChevronDownIcon, ChevronUpIcon, PlayIcon, PlusIcon } from '@radix-ui/react-icons' import { CounterClockwiseClockIcon, CodeIcon } from '@radix-ui/react-icons' import { MethodBadge } from '@/components/api/method-badge' import { flattenSchema, generateExampleJson, type SchemaField } from '@/lib/schema' import { generateCurl } from '@/lib/snippet-generators' +import { JsonEditor } from '@/components/api/json-editor' import styles from './playground-dialog.module.css' type AuthScheme = { @@ -75,6 +76,7 @@ export function PlaygroundDialog({ const [headerValues, setHeaderValues] = useState>({}) const [pathValues, setPathValues] = useState>({}) const [queryValues, setQueryValues] = useState>({}) + const [jsonMode, setJsonMode] = useState(false) const [bodyValues, setBodyValues] = useState>(() => { if (!body) return {} const init: Record = {} @@ -85,10 +87,12 @@ export function PlaygroundDialog({ } return init }) + const [bodyJsonStr, setBodyJsonStr] = useState(() => body ? body.jsonExample : '{}') const [responseData, setResponseData] = useState<{ - status: number; statusText: string; body: unknown; time: number + status: number; statusText: string; body: unknown; headers?: Record; time: number } | null>(null) + const [responseView, setResponseView] = useState<'body' | 'headers'>('body') const [loading, setLoading] = useState(false) const [collapsed, setCollapsed] = useState>({}) @@ -153,7 +157,11 @@ export function PlaygroundDialog({ let reqBody: unknown = undefined if (body) { reqHeaders['Content-Type'] = body.contentType ?? 'application/json' - reqBody = bodyValues + if (jsonMode) { + try { reqBody = JSON.parse(bodyJsonStr) } catch { reqBody = bodyValues } + } else { + reqBody = bodyValues + } } try { @@ -335,7 +343,14 @@ export function PlaygroundDialog({
Body
- + { + if (!jsonMode) { + setBodyJsonStr(JSON.stringify(bodyValues, null, 2)) + } else { + try { setBodyValues(JSON.parse(bodyJsonStr)) } catch { /* ignore */ } + } + setJsonMode(!jsonMode) + }}> toggleCollapse('body')}> @@ -343,14 +358,25 @@ export function PlaygroundDialog({
- {!collapsed.body && body.fields.map((f) => ( - setBodyValues({ ...bodyValues, [f.name]: val })} - /> - ))} + {!collapsed.body && ( + jsonMode ? ( +
+ setBodyJsonStr(val)} + /> +
+ ) : ( + body.fields.map((f) => ( + setBodyValues({ ...bodyValues, [f.name]: val })} + /> + )) + ) + )} )} @@ -360,6 +386,17 @@ export function PlaygroundDialog({
Response + {responseData && ( + + } />}> + {responseView === 'body' ? 'Body' : 'Headers'} + + + setResponseView('body')}>Body + setResponseView('headers')}>Headers + + + )}
{responseData ? ( @@ -373,14 +410,29 @@ export function PlaygroundDialog({ Time : {responseData.time} ms
-
-
- {responseLines.map((_, i) => ( -
{i + 1}
- ))} + {responseView === 'body' ? ( +
+
+ {responseLines.map((_, i) => ( +
{i + 1}
+ ))} +
+
{responseJson}
-
{responseJson}
-
+ ) : ( +
+ {responseData.headers ? ( + Object.entries(responseData.headers).map(([k, v]) => ( +
+ {k} + {v} +
+ )) + ) : ( +
No headers available
+ )} +
+ )} ) : (
diff --git a/packages/chronicle/src/components/api/json-editor.tsx b/packages/chronicle/src/components/api/json-editor.tsx index 730e9028..fb0e2afb 100644 --- a/packages/chronicle/src/components/api/json-editor.tsx +++ b/packages/chronicle/src/components/api/json-editor.tsx @@ -17,9 +17,11 @@ interface JsonEditorProps { export function JsonEditor({ value, onChange, readOnly }: JsonEditorProps) { const containerRef = useRef(null) const viewRef = useRef(null) + const onChangeRef = useRef(onChange) const { theme } = useTheme() const isDark = theme === 'dark' + onChangeRef.current = onChange useEffect(() => { if (!containerRef.current) return @@ -30,13 +32,11 @@ export function JsonEditor({ value, onChange, readOnly }: JsonEditorProps) { EditorView.lineWrapping, ...(isDark ? [oneDark] : []), ...(readOnly ? [EditorState.readOnly.of(true)] : []), - ...(onChange - ? [EditorView.updateListener.of((update) => { - if (update.docChanged) { - onChange(update.state.doc.toString()) - } - })] - : []), + EditorView.updateListener.of((update) => { + if (update.docChanged && onChangeRef.current) { + onChangeRef.current(update.state.doc.toString()) + } + }), ] const state = EditorState.create({ doc: value, extensions }) @@ -44,7 +44,7 @@ export function JsonEditor({ value, onChange, readOnly }: JsonEditorProps) { viewRef.current = view return () => view.destroy() - }, [isDark, readOnly, onChange]) + }, [isDark, readOnly]) useEffect(() => { const view = viewRef.current diff --git a/packages/chronicle/src/server/api/apis-proxy.ts b/packages/chronicle/src/server/api/apis-proxy.ts index 3089aa6d..3619178c 100644 --- a/packages/chronicle/src/server/api/apis-proxy.ts +++ b/packages/chronicle/src/server/api/apis-proxy.ts @@ -51,10 +51,14 @@ export default defineHandler(async event => { ? await response.json() : await response.text(); + const responseHeaders: Record = {}; + response.headers.forEach((v, k) => { responseHeaders[k] = v; }); + return Response.json({ status: response.status, statusText: response.statusText, - body: responseBody + body: responseBody, + headers: responseHeaders }); } catch (error) { const message = From 633126656ddd10b5804289285ed08c2e9c25e017 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 12 May 2026 15:20:30 +0530 Subject: [PATCH 08/15] chore: remove old API components replaced by api-v2 Removed: endpoint-page, field-section, field-row, key-value-editor, code-snippets, response-panel. Kept: method-badge, json-editor (still used by api-v2 and other components). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/api/code-snippets.module.css | 7 - .../src/components/api/code-snippets.tsx | 76 ----- .../components/api/endpoint-page.module.css | 58 ---- .../src/components/api/endpoint-page.tsx | 283 ------------------ .../src/components/api/field-row.module.css | 126 -------- .../src/components/api/field-row.tsx | 204 ------------- .../components/api/field-section.module.css | 24 -- .../src/components/api/field-section.tsx | 100 ------- .../chronicle/src/components/api/index.ts | 6 - .../api/key-value-editor.module.css | 13 - .../src/components/api/key-value-editor.tsx | 62 ---- .../components/api/response-panel.module.css | 8 - .../src/components/api/response-panel.tsx | 44 --- 13 files changed, 1011 deletions(-) delete mode 100644 packages/chronicle/src/components/api/code-snippets.module.css delete mode 100644 packages/chronicle/src/components/api/code-snippets.tsx delete mode 100644 packages/chronicle/src/components/api/endpoint-page.module.css delete mode 100644 packages/chronicle/src/components/api/endpoint-page.tsx delete mode 100644 packages/chronicle/src/components/api/field-row.module.css delete mode 100644 packages/chronicle/src/components/api/field-row.tsx delete mode 100644 packages/chronicle/src/components/api/field-section.module.css delete mode 100644 packages/chronicle/src/components/api/field-section.tsx delete mode 100644 packages/chronicle/src/components/api/key-value-editor.module.css delete mode 100644 packages/chronicle/src/components/api/key-value-editor.tsx delete mode 100644 packages/chronicle/src/components/api/response-panel.module.css delete mode 100644 packages/chronicle/src/components/api/response-panel.tsx diff --git a/packages/chronicle/src/components/api/code-snippets.module.css b/packages/chronicle/src/components/api/code-snippets.module.css deleted file mode 100644 index 2ec0429e..00000000 --- a/packages/chronicle/src/components/api/code-snippets.module.css +++ /dev/null @@ -1,7 +0,0 @@ -.snippets { - width: 100%; -} - -.snippets :global([class*="code-block-module_header"]) { - justify-content: space-between; -} diff --git a/packages/chronicle/src/components/api/code-snippets.tsx b/packages/chronicle/src/components/api/code-snippets.tsx deleted file mode 100644 index 84ed9daf..00000000 --- a/packages/chronicle/src/components/api/code-snippets.tsx +++ /dev/null @@ -1,76 +0,0 @@ -"use client"; - -import { useMemo, useState } from "react"; -import { CodeBlock } from "@raystack/apsara"; -import { - generateCurl, - generatePython, - generateGo, - generateTypeScript, -} from "@/lib/snippet-generators"; -import styles from "./code-snippets.module.css"; - -interface CodeSnippetsProps { - method: string; - url: string; - headers: Record; - body?: string; -} - -const languages = [ - { value: "curl", label: "cURL", lang: "curl", generate: generateCurl }, - { - value: "python", - label: "Python", - lang: "python", - generate: generatePython, - }, - { value: "go", label: "Go", lang: "go", generate: generateGo }, - { - value: "typescript", - label: "TypeScript", - lang: "typescript", - generate: generateTypeScript, - }, -]; - -export function CodeSnippets({ - method, - url, - headers, - body, -}: CodeSnippetsProps) { - const opts = { method, url, headers, body }; - const [selected, setSelected] = useState("curl"); - const current = languages.find((l) => l.value === selected) ?? languages[0]; - - const code = useMemo( - () => current.generate(opts), - [selected, method, url, headers, body], - ); - - return ( - - - - - - {languages.map((l) => ( - - {l.label} - - ))} - - - - - - {code} - - - ); -} diff --git a/packages/chronicle/src/components/api/endpoint-page.module.css b/packages/chronicle/src/components/api/endpoint-page.module.css deleted file mode 100644 index a28a838b..00000000 --- a/packages/chronicle/src/components/api/endpoint-page.module.css +++ /dev/null @@ -1,58 +0,0 @@ -.layout { - display: grid; - grid-template-columns: 1fr 1fr; - gap: var(--rs-space-9); - padding: var(--rs-space-7); - max-width: 1400px; -} - -.left { - gap: var(--rs-space-7); - min-width: 0; -} - -.right { - gap: var(--rs-space-5); - min-width: 0; -} - -.title { - margin: 0; -} - -.description { - color: var(--rs-color-foreground-base-secondary); -} - -.methodPath { - gap: var(--rs-space-3); - padding: var(--rs-space-4) var(--rs-space-5); - border: 1px solid var(--rs-color-border-base-primary); - border-radius: 8px; - background: var(--rs-color-background-base-secondary); - overflow: hidden; -} - -.path { - font-family: monospace; - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.tryButton { - margin-left: auto; - flex-shrink: 0; -} - -@media (max-width: 900px) { - .layout { - grid-template-columns: 1fr; - } - - .right { - position: static; - } -} diff --git a/packages/chronicle/src/components/api/endpoint-page.tsx b/packages/chronicle/src/components/api/endpoint-page.tsx deleted file mode 100644 index 3b13dce9..00000000 --- a/packages/chronicle/src/components/api/endpoint-page.tsx +++ /dev/null @@ -1,283 +0,0 @@ -'use client' - -import { useState, useCallback } from 'react' -import type { OpenAPIV3 } from 'openapi-types' -import { Flex, Text, Headline, Button, CodeBlock } from '@raystack/apsara' -import { MethodBadge } from './method-badge' -import { FieldSection } from './field-section' -import { KeyValueEditor, type KeyValueEntry } from './key-value-editor' -import { CodeSnippets } from './code-snippets' -import { ResponsePanel } from './response-panel' -import { flattenSchema, generateExampleJson, type SchemaField } from '@/lib/schema' -import styles from './endpoint-page.module.css' - -interface EndpointPageProps { - method: string - path: string - operation: OpenAPIV3.OperationObject - serverUrl: string - specName: string - auth?: { type: string; header: string; placeholder?: string } -} - -export function EndpointPage({ method, path, operation, serverUrl, specName, auth }: EndpointPageProps) { - const params = (operation.parameters ?? []) as OpenAPIV3.ParameterObject[] - const body = getRequestBody(operation.requestBody as OpenAPIV3.RequestBodyObject | undefined) - - const headerFields = paramsToFields(params.filter((p) => p.in === 'header')) - const headerLocations = Object.fromEntries(headerFields.map((f) => [f.name, 'header'])) - const pathFields = paramsToFields(params.filter((p) => p.in === 'path')) - const pathLocations = Object.fromEntries(pathFields.map((f) => [f.name, 'path'])) - const queryFields = paramsToFields(params.filter((p) => p.in === 'query')) - const queryLocations = Object.fromEntries(queryFields.map((f) => [f.name, 'query'])) - const responses = getResponseSections(operation.responses as Record) - - // State for editable fields - const [customHeaders, setCustomHeaders] = useState(() => { - const initial: KeyValueEntry[] = [] - if (auth) initial.push({ key: auth.header, value: '' }) - return initial - }) - const [headerValues, setHeaderValues] = useState>({}) - const [pathValues, setPathValues] = useState>({}) - const [queryValues, setQueryValues] = useState>({}) - const [bodyValues, setBodyValues] = useState>(() => { - try { return body?.jsonExample ? JSON.parse(body.jsonExample) : {} } - catch { return {} } - }) - const [bodyJsonStr, setBodyJsonStr] = useState(body?.jsonExample ?? '{}') - const [responseBody, setResponseBody] = useState<{ status: number; statusText: string; body: unknown } | null>(null) - const [loading, setLoading] = useState(false) - - // Two-way sync: fields → JSON - const handleBodyValuesChange = useCallback((values: Record) => { - setBodyValues(values) - setBodyJsonStr(JSON.stringify(values, null, 2)) - }, []) - - // Two-way sync: JSON → fields - const handleBodyJsonChange = useCallback((jsonStr: string) => { - setBodyJsonStr(jsonStr) - try { - setBodyValues(JSON.parse(jsonStr)) - } catch { /* ignore invalid JSON while typing */ } - }, []) - - // Try it handler - const handleTryIt = useCallback(async () => { - setLoading(true) - setResponseBody(null) - - let resolvedPath = path - for (const [key, value] of Object.entries(pathValues)) { - resolvedPath = resolvedPath.replace(`{${key}}`, encodeURIComponent(String(value))) - } - - const queryEntries = Object.entries(queryValues).filter(([, v]) => v !== undefined && v !== '') - const queryString = queryEntries - .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`) - .join('&') - const fullPath = queryString ? `${resolvedPath}?${queryString}` : resolvedPath - - const reqHeaders: Record = {} - for (const [key, value] of Object.entries(headerValues)) { - if (value !== undefined && value !== null && value !== '') reqHeaders[key] = String(value) - } - for (const entry of customHeaders) { - if (entry.key && entry.value) reqHeaders[entry.key] = entry.value - } - if (body && bodyJsonStr) { - reqHeaders['Content-Type'] = body.contentType ?? 'application/json' - } - - try { - const res = await fetch('/api/apis-proxy', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - specName, - method, - path: fullPath, - headers: reqHeaders, - body: body ? bodyValues : undefined, - }), - }) - const data = await res.json() - if (data.status !== undefined) { - setResponseBody(data) - } else { - setResponseBody({ status: res.status, statusText: res.statusText, body: data.error ?? data }) - } - } catch (err) { - console.error('API request failed:', err) - setResponseBody({ status: 0, statusText: 'Error', body: 'Failed to send request' }) - } finally { - setLoading(false) - } - }, [specName, method, path, pathValues, queryValues, headerValues, customHeaders, bodyValues, bodyJsonStr, body]) - - // Snippet display values - const fullUrl = '{domain}' + path - const snippetHeaders: Record = {} - if (auth) { - snippetHeaders[auth.header] = auth.placeholder ?? 'YOUR_API_KEY' - } - if (body) { - snippetHeaders['Content-Type'] = body.contentType ?? 'application/json' - } - - return ( -
- - {operation.summary && ( - {operation.summary} - )} - {operation.description && ( - {operation.description} - )} - - - - {path} - - - - - - - - - {body && ( - - )} - - {responses.map((resp) => ( - - ))} - - - - - {responseBody && ( - - - Response — {responseBody.status} {responseBody.statusText} - - - - - - - - {typeof responseBody.body === 'string' - ? (responseBody.body || 'No response body') - : (JSON.stringify(responseBody.body, null, 2) ?? 'No response body')} - - - - - )} - -
- ) -} - -function paramsToFields(params: OpenAPIV3.ParameterObject[]): SchemaField[] { - return params.map((p) => { - const schema = (p.schema ?? {}) as OpenAPIV3.SchemaObject - return { - name: p.name, - type: schema.type ? String(schema.type) : 'string', - required: p.required ?? false, - description: p.description, - default: schema.default, - } - }) -} - -interface RequestBody { - contentType: string - fields: SchemaField[] - jsonExample: string -} - -function getRequestBody(body: OpenAPIV3.RequestBodyObject | undefined): RequestBody | null { - if (!body?.content) return null - const contentType = Object.keys(body.content)[0] - if (!contentType) return null - const schema = body.content[contentType]?.schema as OpenAPIV3.SchemaObject | undefined - if (!schema) return null - return { - contentType, - fields: flattenSchema(schema), - jsonExample: JSON.stringify(generateExampleJson(schema), null, 2), - } -} - -interface ResponseSection { - status: string - description?: string - fields: SchemaField[] - jsonExample?: string -} - -function getResponseSections(responses: Record): ResponseSection[] { - return Object.entries(responses).map(([status, resp]) => { - const content = resp.content ?? {} - const contentType = Object.keys(content)[0] - const schema = contentType - ? (content[contentType]?.schema as OpenAPIV3.SchemaObject | undefined) - : undefined - - return { - status, - description: resp.description, - fields: schema ? flattenSchema(schema) : [], - jsonExample: schema ? JSON.stringify(generateExampleJson(schema), null, 2) : undefined, - } - }) -} diff --git a/packages/chronicle/src/components/api/field-row.module.css b/packages/chronicle/src/components/api/field-row.module.css deleted file mode 100644 index 44c12344..00000000 --- a/packages/chronicle/src/components/api/field-row.module.css +++ /dev/null @@ -1,126 +0,0 @@ -.row { - display: flex; - flex-direction: column; - gap: var(--rs-space-2); - padding: var(--rs-space-4) 0; -} - -.row + .row { - border-top: 1px solid var(--rs-color-border-base-primary); -} - -.main { - gap: var(--rs-space-2); -} - -.badges { - gap: var(--rs-space-3); - flex-wrap: wrap; -} - -.name { - font-family: monospace; - font-size: 13px; - color: var(--rs-color-foreground-base-primary); -} - -.type { - font-family: monospace; - font-size: 12px; - padding: 1px var(--rs-space-2); - border-radius: 4px; - background: var(--rs-color-background-neutral-secondary); - color: var(--rs-color-foreground-base-secondary); -} - -.location { - font-size: 12px; - padding: 1px var(--rs-space-2); - border-radius: 4px; - background: var(--rs-color-background-neutral-secondary); - color: var(--rs-color-foreground-base-secondary); -} - -.required { - font-size: 11px; - padding: 1px var(--rs-space-2); - border-radius: 4px; - background: var(--rs-color-background-danger-primary); - color: var(--rs-color-foreground-danger-primary); -} - -.description { - color: var(--rs-color-foreground-base-secondary); - font-size: 13px; -} - -.example { - color: var(--rs-color-foreground-base-secondary); - font-size: 12px; -} - -.example code { - font-family: monospace; - background: var(--rs-color-background-neutral-secondary); - padding: 1px var(--rs-space-2); - border-radius: 3px; -} - -.accordion { - border: none; -} - -.accordion :global([class*="accordion-header"]) { - margin: 0; -} - -.accordion button { - min-height: unset; - padding: 0; - border: none; - background: transparent; - box-shadow: none; -} - -.accordion button:hover, -.accordion button:focus-visible { - background: transparent; -} - -.accordion :global([class*="accordion-content-inner"]) { - padding: var(--rs-space-3) 0 0 0; - border: none; - box-shadow: none; -} - -.children { - display: flex; - flex-direction: column; - padding-left: var(--rs-space-5); - width: 100%; -} - -.trigger { - color: var(--rs-color-foreground-base-secondary); -} - -.fieldInfo { - flex: 1; - min-width: 0; -} - -.fieldInput { - flex: 1; - min-width: 0; -} - -.arrayItems { - gap: var(--rs-space-3); -} - -.arrayItem { - align-items: center; - border: 1px solid var(--rs-color-border-base-primary); - border-radius: 8px; - padding: var(--rs-space-3) var(--rs-space-4); -} diff --git a/packages/chronicle/src/components/api/field-row.tsx b/packages/chronicle/src/components/api/field-row.tsx deleted file mode 100644 index fb7d1641..00000000 --- a/packages/chronicle/src/components/api/field-row.tsx +++ /dev/null @@ -1,204 +0,0 @@ -'use client' - -import { Flex, Text, Accordion, InputField, Switch, Select, IconButton } from '@raystack/apsara' -import { TrashIcon, PlusIcon } from '@heroicons/react/24/outline' -import type { SchemaField } from '@/lib/schema' -import styles from './field-row.module.css' - -interface FieldRowProps { - field: SchemaField - location?: string - editable?: boolean - value?: unknown - onChange?: (name: string, value: unknown) => void -} - -export function FieldRow({ field, location, editable, value, onChange }: FieldRowProps) { - const hasChildren = field.children && field.children.length > 0 - const isArray = field.type.endsWith('[]') - - const label = ( - - {field.name} - {field.type} - {location && {location}} - {field.required && required} - - ) - - if (hasChildren && !isArray) { - const objValue = (value ?? {}) as Record - return ( -
- - - {label} - -
- {field.children!.map((child) => ( - { - onChange?.(field.name, { ...objValue, [name]: val }) - } : undefined} - /> - ))} -
-
-
-
-
- ) - } - - if (isArray && editable) { - const items = (Array.isArray(value) ? value : []) as unknown[] - const itemChildren = field.children - - return ( -
- - - {label} - { - const newItem = itemChildren ? {} : '' - onChange?.(field.name, [...items, newItem]) - }}> - - - - {field.description && {field.description}} - - {items.map((item, i) => ( - - {itemChildren ? ( - - {itemChildren.map((child) => ( - )?.[child.name]} - onChange={(name, val) => { - const updated = [...items] - updated[i] = { ...(updated[i] as Record), [name]: val } - onChange?.(field.name, updated) - }} - /> - ))} - - ) : ( - { - const updated = [...items] - updated[i] = val - onChange?.(field.name, updated) - }} - /> - )} - { - const updated = items.filter((_, j) => j !== i) - onChange?.(field.name, updated) - }}> - - - - ))} - - -
- ) - } - - // Leaf field — inline layout - return ( -
- - - {label} - {field.description && {field.description}} - - {editable ? ( -
- -
- ) : ( - field.default !== undefined && ( - - Default: {JSON.stringify(field.default)} - - ) - )} -
-
- ) -} - -function EditableInput({ - field, - value, - onChange, -}: { - field: SchemaField - value: unknown - onChange?: (name: string, value: unknown) => void -}) { - if (field.enum) { - const enumMap = new Map(field.enum.map((opt) => [String(opt), opt])) - return ( - - ) - } - - const baseType = field.type.replace('[]', '').replace(/\(.*\)/, '') - - if (baseType === 'boolean') { - return ( - onChange?.(field.name, checked)} - /> - ) - } - - if (baseType === 'integer' || baseType === 'number') { - return ( - onChange?.(field.name, Number(e.target.value))} - /> - ) - } - - return ( - onChange?.(field.name, e.target.value)} - /> - ) -} diff --git a/packages/chronicle/src/components/api/field-section.module.css b/packages/chronicle/src/components/api/field-section.module.css deleted file mode 100644 index 279e047b..00000000 --- a/packages/chronicle/src/components/api/field-section.module.css +++ /dev/null @@ -1,24 +0,0 @@ -.header { - padding-bottom: var(--rs-space-3); -} - -.label { - font-family: monospace; - color: var(--rs-color-foreground-base-secondary); - font-size: 13px; -} - -.separator { - height: 1px; - background: var(--rs-color-border-base-primary); -} - -.tabs { - margin-top: var(--rs-space-3); -} - -.noFields { - color: var(--rs-color-foreground-base-secondary); - padding: var(--rs-space-5); -} - diff --git a/packages/chronicle/src/components/api/field-section.tsx b/packages/chronicle/src/components/api/field-section.tsx deleted file mode 100644 index 7c484c07..00000000 --- a/packages/chronicle/src/components/api/field-section.tsx +++ /dev/null @@ -1,100 +0,0 @@ -'use client' - -import { Flex, Text, Tabs, CodeBlock } from '@raystack/apsara' -import type { SchemaField } from '@/lib/schema' -import { FieldRow } from './field-row' -import { JsonEditor } from './json-editor' -import styles from './field-section.module.css' - -interface FieldSectionProps { - title: string - label?: string - fields: SchemaField[] - locations?: Record - jsonExample?: string - editableJson?: boolean - onJsonChange?: (value: string) => void - alwaysShow?: boolean - editable?: boolean - values?: Record - onValuesChange?: (values: Record) => void - children?: React.ReactNode -} - -export function FieldSection({ - title, label, fields, locations, jsonExample, - editableJson, onJsonChange, alwaysShow, - editable, values, onValuesChange, children, -}: FieldSectionProps) { - if (fields.length === 0 && !children && !alwaysShow) return null - - const fieldsContent = fields.length > 0 ? ( - - {fields.map((field) => ( - { - onValuesChange?.({ ...values, [name]: val }) - } : undefined} - /> - ))} - - ) : !children ? ( - No fields defined - ) : null - - if (jsonExample !== undefined || alwaysShow) { - return ( - - - {title} - {label && {label}} - -
- - - Fields - JSON - - - {fieldsContent} - {children} - - - {editableJson ? ( - - ) : ( - - - - - - {jsonExample ?? '{}'} - - - )} - - - - ) - } - - return ( - - - {title} - {label && {label}} - -
- {fieldsContent} - {children} - - ) -} diff --git a/packages/chronicle/src/components/api/index.ts b/packages/chronicle/src/components/api/index.ts index 26711154..80a05b57 100644 --- a/packages/chronicle/src/components/api/index.ts +++ b/packages/chronicle/src/components/api/index.ts @@ -1,8 +1,2 @@ -export { EndpointPage } from './endpoint-page' export { MethodBadge } from './method-badge' -export { FieldSection } from './field-section' -export { FieldRow } from './field-row' -export { CodeSnippets } from './code-snippets' -export { ResponsePanel } from './response-panel' export { JsonEditor } from './json-editor' -export { KeyValueEditor } from './key-value-editor' diff --git a/packages/chronicle/src/components/api/key-value-editor.module.css b/packages/chronicle/src/components/api/key-value-editor.module.css deleted file mode 100644 index a972cdce..00000000 --- a/packages/chronicle/src/components/api/key-value-editor.module.css +++ /dev/null @@ -1,13 +0,0 @@ -.editor { - padding: var(--rs-space-3) 0; -} - -.row { - width: 100%; -} - -.input { - flex: 1; - min-width: 0; -} - diff --git a/packages/chronicle/src/components/api/key-value-editor.tsx b/packages/chronicle/src/components/api/key-value-editor.tsx deleted file mode 100644 index ed8ce391..00000000 --- a/packages/chronicle/src/components/api/key-value-editor.tsx +++ /dev/null @@ -1,62 +0,0 @@ -'use client' - -import { Flex, InputField, IconButton, Button } from '@raystack/apsara' -import { TrashIcon, PlusIcon } from '@heroicons/react/24/outline' -import styles from './key-value-editor.module.css' - -export interface KeyValueEntry { - key: string - value: string -} - -interface KeyValueEditorProps { - entries: KeyValueEntry[] - onChange: (entries: KeyValueEntry[]) => void -} - -export function KeyValueEditor({ entries, onChange }: KeyValueEditorProps) { - const updateEntry = (index: number, field: 'key' | 'value', val: string) => { - const updated = [...entries] - updated[index] = { ...updated[index], [field]: val } - onChange(updated) - } - - const removeEntry = (index: number) => { - onChange(entries.filter((_, i) => i !== index)) - } - - const addEntry = () => { - onChange([...entries, { key: '', value: '' }]) - } - - return ( - - {entries.map((entry, i) => ( - -
- updateEntry(i, 'key', e.target.value)} - /> -
-
- updateEntry(i, 'value', e.target.value)} - /> -
- removeEntry(i)}> - - -
- ))} - -
- ) -} diff --git a/packages/chronicle/src/components/api/response-panel.module.css b/packages/chronicle/src/components/api/response-panel.module.css deleted file mode 100644 index 55f1b581..00000000 --- a/packages/chronicle/src/components/api/response-panel.module.css +++ /dev/null @@ -1,8 +0,0 @@ -.panel { - width: 100%; -} - -/* stylelint-disable-next-line selector-pseudo-class-no-unknown */ -.panel :global([class*="code-block-module_header"]) { - justify-content: space-between; -} diff --git a/packages/chronicle/src/components/api/response-panel.tsx b/packages/chronicle/src/components/api/response-panel.tsx deleted file mode 100644 index f1b9e05d..00000000 --- a/packages/chronicle/src/components/api/response-panel.tsx +++ /dev/null @@ -1,44 +0,0 @@ -'use client' - -import { CodeBlock } from '@raystack/apsara' -import styles from './response-panel.module.css' - -interface ResponsePanelProps { - responses: { - status: string - description?: string - jsonExample?: string - }[] -} - -export function ResponsePanel({ responses }: ResponsePanelProps) { - const withExamples = responses.filter((r) => r.jsonExample) - if (withExamples.length === 0) return null - - const defaultValue = withExamples[0].status - - return ( - - - - - - {withExamples.map((resp) => ( - - {resp.status} {resp.description ?? resp.status} - - ))} - - - - - - {withExamples.map((resp) => ( - - {resp.jsonExample!} - - ))} - - - ) -} From 0912fadaaa64273237a7aad456f13183a7f01154 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 12 May 2026 15:30:24 +0530 Subject: [PATCH 09/15] feat: redesign API sidebar and fix method colors - Flat tag groups with plain text headers (not collapsible accordions) - Method text right-aligned in mono font with semantic colors - GET=green (success), POST=blue (accent), PUT=yellow, DELETE=red - Active item with neutral background highlight - Consistent method colors across sidebar, method bar, and playground Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/api/method-badge.tsx | 4 +- .../src/themes/default/Layout.module.css | 64 +++++++++++++++++ .../chronicle/src/themes/default/Layout.tsx | 72 +++++++++++++++++-- 3 files changed, 133 insertions(+), 7 deletions(-) diff --git a/packages/chronicle/src/components/api/method-badge.tsx b/packages/chronicle/src/components/api/method-badge.tsx index 278f3e7b..8f231ac2 100644 --- a/packages/chronicle/src/components/api/method-badge.tsx +++ b/packages/chronicle/src/components/api/method-badge.tsx @@ -6,8 +6,8 @@ import styles from './method-badge.module.css' type BadgeVariant = 'accent' | 'danger' | 'success' | 'neutral' | 'warning' | 'gradient' const methodVariants: Record = { - GET: 'accent', - POST: 'success', + GET: 'success', + POST: 'accent', PUT: 'warning', DELETE: 'danger', PATCH: 'neutral', diff --git a/packages/chronicle/src/themes/default/Layout.module.css b/packages/chronicle/src/themes/default/Layout.module.css index 911a157f..3f59d269 100644 --- a/packages/chronicle/src/themes/default/Layout.module.css +++ b/packages/chronicle/src/themes/default/Layout.module.css @@ -226,3 +226,67 @@ .page { padding: var(--rs-space-2) 0; } + +.apiGroup { + display: flex; + flex-direction: column; + gap: var(--rs-space-3); + margin-top: var(--rs-space-8); + width: 100%; +} + +.apiGroup:first-child { + margin-top: 0; +} + +.apiGroupLabel { + font-size: var(--rs-font-size-small); + font-weight: var(--rs-font-weight-medium); + line-height: var(--rs-line-height-small); + letter-spacing: var(--rs-letter-spacing-small); + color: var(--rs-color-foreground-base-secondary); + padding: 0 var(--rs-space-3); +} + +.apiGroupItems { + display: flex; + flex-direction: column; +} + +.apiItem { + display: flex; + align-items: center; + gap: var(--rs-space-3); + padding: var(--rs-space-3); + border-radius: var(--rs-radius-2); + text-decoration: none; + cursor: pointer; + white-space: nowrap; +} + +.apiItem:hover { + background: var(--rs-color-background-neutral-secondary); +} + +.apiItemActive { + background: var(--rs-color-background-neutral-secondary); +} + +.apiItemName { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + font-size: var(--rs-font-size-small); + font-weight: var(--rs-font-weight-medium); + line-height: var(--rs-line-height-small); + letter-spacing: var(--rs-letter-spacing-small); + color: var(--rs-color-foreground-base-primary); +} + +.apiMethodText { + font-family: var(--rs-font-mono); + font-size: var(--rs-font-size-mono-mini); + line-height: var(--rs-line-height-mini); + flex-shrink: 0; +} diff --git a/packages/chronicle/src/themes/default/Layout.tsx b/packages/chronicle/src/themes/default/Layout.tsx index 190cff7f..68c84845 100644 --- a/packages/chronicle/src/themes/default/Layout.tsx +++ b/packages/chronicle/src/themes/default/Layout.tsx @@ -155,11 +155,19 @@ export function Layout({
) : null} {tree.children.map((item, i) => ( - + isApiRoute ? ( + + ) : ( + + ) ))} {config.versions?.length ? ( @@ -271,6 +279,60 @@ function SidebarNode({ ); } +const methodColorMap: Record = { + 'method-get': 'var(--rs-color-foreground-success-primary)', + 'method-post': 'var(--rs-color-foreground-accent-primary)', + 'method-put': 'var(--rs-color-foreground-attention-primary)', + 'method-delete': 'var(--rs-color-foreground-danger-primary)', + 'method-patch': 'var(--rs-color-foreground-base-secondary)', +}; + +const methodLabelMap: Record = { + 'method-get': 'GET', + 'method-post': 'POST', + 'method-put': 'PUT', + 'method-delete': 'DEL', + 'method-patch': 'PATCH', +}; + +function ApiSidebarNode({ item, pathname }: { item: Node; pathname: string }) { + if (item.type === 'separator') return null; + + if (item.type === 'folder') { + return ( +
+ {item.name?.toString()} +
+ {item.children.map((child, i) => ( + + ))} +
+
+ ); + } + + const isActive = pathname === item.url; + const href = item.url ?? '#'; + const iconKey = typeof item.icon === 'string' ? item.icon : ''; + const methodLabel = methodLabelMap[iconKey]; + const methodColor = methodColorMap[iconKey]; + + return ( + + {item.name} + {methodLabel && ( + + {methodLabel} + + )} + + ); +} + function TestRequestButton() { const match = useApiOperation(); const [open, setOpen] = useState(false); From b1ebc1c866b06ee2262bfb964522d362d4df4d8b Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 12 May 2026 15:34:20 +0530 Subject: [PATCH 10/15] refactor: rename api-v2 to api, use Flex for sidebar - Merge method-badge and json-editor into api folder - Remove api-v2 directory, update all imports - Use Flex components for API sidebar nodes Co-Authored-By: Claude Opus 4.6 (1M context) --- .../chronicle/src/components/api-v2/index.ts | 5 ----- .../api-code-snippet.module.css | 0 .../{api-v2 => api}/api-code-snippet.tsx | 0 .../{api-v2 => api}/api-field-list.module.css | 0 .../{api-v2 => api}/api-field-list.tsx | 0 .../{api-v2 => api}/api-overview.module.css | 0 .../{api-v2 => api}/api-overview.tsx | 0 .../api-response-panel.module.css | 0 .../{api-v2 => api}/api-response-panel.tsx | 0 .../chronicle/src/components/api/index.ts | 5 +++++ .../playground-dialog.module.css | 0 .../{api-v2 => api}/playground-dialog.tsx | 0 packages/chronicle/src/pages/ApiPage.tsx | 2 +- .../src/themes/default/Layout.module.css | 11 ----------- .../chronicle/src/themes/default/Layout.tsx | 19 ++++++++++++------- 15 files changed, 18 insertions(+), 24 deletions(-) delete mode 100644 packages/chronicle/src/components/api-v2/index.ts rename packages/chronicle/src/components/{api-v2 => api}/api-code-snippet.module.css (100%) rename packages/chronicle/src/components/{api-v2 => api}/api-code-snippet.tsx (100%) rename packages/chronicle/src/components/{api-v2 => api}/api-field-list.module.css (100%) rename packages/chronicle/src/components/{api-v2 => api}/api-field-list.tsx (100%) rename packages/chronicle/src/components/{api-v2 => api}/api-overview.module.css (100%) rename packages/chronicle/src/components/{api-v2 => api}/api-overview.tsx (100%) rename packages/chronicle/src/components/{api-v2 => api}/api-response-panel.module.css (100%) rename packages/chronicle/src/components/{api-v2 => api}/api-response-panel.tsx (100%) rename packages/chronicle/src/components/{api-v2 => api}/playground-dialog.module.css (100%) rename packages/chronicle/src/components/{api-v2 => api}/playground-dialog.tsx (100%) diff --git a/packages/chronicle/src/components/api-v2/index.ts b/packages/chronicle/src/components/api-v2/index.ts deleted file mode 100644 index 40dc9eff..00000000 --- a/packages/chronicle/src/components/api-v2/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { ApiOverview } from './api-overview' -export { ApiFieldSection } from './api-field-list' -export { ApiCodeSnippet } from './api-code-snippet' -export { ApiResponsePanel } from './api-response-panel' -export { PlaygroundDialog } from './playground-dialog' diff --git a/packages/chronicle/src/components/api-v2/api-code-snippet.module.css b/packages/chronicle/src/components/api/api-code-snippet.module.css similarity index 100% rename from packages/chronicle/src/components/api-v2/api-code-snippet.module.css rename to packages/chronicle/src/components/api/api-code-snippet.module.css diff --git a/packages/chronicle/src/components/api-v2/api-code-snippet.tsx b/packages/chronicle/src/components/api/api-code-snippet.tsx similarity index 100% rename from packages/chronicle/src/components/api-v2/api-code-snippet.tsx rename to packages/chronicle/src/components/api/api-code-snippet.tsx diff --git a/packages/chronicle/src/components/api-v2/api-field-list.module.css b/packages/chronicle/src/components/api/api-field-list.module.css similarity index 100% rename from packages/chronicle/src/components/api-v2/api-field-list.module.css rename to packages/chronicle/src/components/api/api-field-list.module.css diff --git a/packages/chronicle/src/components/api-v2/api-field-list.tsx b/packages/chronicle/src/components/api/api-field-list.tsx similarity index 100% rename from packages/chronicle/src/components/api-v2/api-field-list.tsx rename to packages/chronicle/src/components/api/api-field-list.tsx diff --git a/packages/chronicle/src/components/api-v2/api-overview.module.css b/packages/chronicle/src/components/api/api-overview.module.css similarity index 100% rename from packages/chronicle/src/components/api-v2/api-overview.module.css rename to packages/chronicle/src/components/api/api-overview.module.css diff --git a/packages/chronicle/src/components/api-v2/api-overview.tsx b/packages/chronicle/src/components/api/api-overview.tsx similarity index 100% rename from packages/chronicle/src/components/api-v2/api-overview.tsx rename to packages/chronicle/src/components/api/api-overview.tsx diff --git a/packages/chronicle/src/components/api-v2/api-response-panel.module.css b/packages/chronicle/src/components/api/api-response-panel.module.css similarity index 100% rename from packages/chronicle/src/components/api-v2/api-response-panel.module.css rename to packages/chronicle/src/components/api/api-response-panel.module.css diff --git a/packages/chronicle/src/components/api-v2/api-response-panel.tsx b/packages/chronicle/src/components/api/api-response-panel.tsx similarity index 100% rename from packages/chronicle/src/components/api-v2/api-response-panel.tsx rename to packages/chronicle/src/components/api/api-response-panel.tsx diff --git a/packages/chronicle/src/components/api/index.ts b/packages/chronicle/src/components/api/index.ts index 80a05b57..5550710a 100644 --- a/packages/chronicle/src/components/api/index.ts +++ b/packages/chronicle/src/components/api/index.ts @@ -1,2 +1,7 @@ +export { ApiOverview } from './api-overview' +export { ApiFieldSection } from './api-field-list' +export { ApiCodeSnippet } from './api-code-snippet' +export { ApiResponsePanel } from './api-response-panel' +export { PlaygroundDialog } from './playground-dialog' export { MethodBadge } from './method-badge' export { JsonEditor } from './json-editor' diff --git a/packages/chronicle/src/components/api-v2/playground-dialog.module.css b/packages/chronicle/src/components/api/playground-dialog.module.css similarity index 100% rename from packages/chronicle/src/components/api-v2/playground-dialog.module.css rename to packages/chronicle/src/components/api/playground-dialog.module.css diff --git a/packages/chronicle/src/components/api-v2/playground-dialog.tsx b/packages/chronicle/src/components/api/playground-dialog.tsx similarity index 100% rename from packages/chronicle/src/components/api-v2/playground-dialog.tsx rename to packages/chronicle/src/components/api/playground-dialog.tsx diff --git a/packages/chronicle/src/pages/ApiPage.tsx b/packages/chronicle/src/pages/ApiPage.tsx index 803271f1..3fe06326 100644 --- a/packages/chronicle/src/pages/ApiPage.tsx +++ b/packages/chronicle/src/pages/ApiPage.tsx @@ -1,6 +1,6 @@ import { Flex, Headline, Text } from '@raystack/apsara'; import type { OpenAPIV3 } from 'openapi-types'; -import { ApiOverview } from '@/components/api-v2'; +import { ApiOverview } from '@/components/api'; import { findApiOperation } from '@/lib/api-routes'; import { Head } from '@/lib/head'; import type { ApiSpec } from '@/lib/openapi'; diff --git a/packages/chronicle/src/themes/default/Layout.module.css b/packages/chronicle/src/themes/default/Layout.module.css index 3f59d269..f5fcef22 100644 --- a/packages/chronicle/src/themes/default/Layout.module.css +++ b/packages/chronicle/src/themes/default/Layout.module.css @@ -228,9 +228,6 @@ } .apiGroup { - display: flex; - flex-direction: column; - gap: var(--rs-space-3); margin-top: var(--rs-space-8); width: 100%; } @@ -248,15 +245,7 @@ padding: 0 var(--rs-space-3); } -.apiGroupItems { - display: flex; - flex-direction: column; -} - .apiItem { - display: flex; - align-items: center; - gap: var(--rs-space-3); padding: var(--rs-space-3); border-radius: var(--rs-radius-2); text-decoration: none; diff --git a/packages/chronicle/src/themes/default/Layout.tsx b/packages/chronicle/src/themes/default/Layout.tsx index 68c84845..8a6eef75 100644 --- a/packages/chronicle/src/themes/default/Layout.tsx +++ b/packages/chronicle/src/themes/default/Layout.tsx @@ -14,7 +14,7 @@ import { Link as RouterLink, useLocation, useNavigate } from 'react-router'; import type { OpenAPIV3 } from 'openapi-types'; import { MethodBadge } from '@/components/api/method-badge'; import { useApiOperation } from '@/lib/use-api-operation'; -import { PlaygroundDialog } from '@/components/api-v2/playground-dialog'; +import { PlaygroundDialog } from '@/components/api/playground-dialog'; import { ClientThemeSwitcher } from '@/components/ui/client-theme-switcher'; import { Search } from '@/components/ui/search'; import { Breadcrumbs } from '@/components/ui/breadcrumbs'; @@ -300,9 +300,9 @@ function ApiSidebarNode({ item, pathname }: { item: Node; pathname: string }) { if (item.type === 'folder') { return ( -
+ {item.name?.toString()} -
+ {item.children.map((child, i) => ( ))} -
-
+
+ ); } @@ -322,14 +322,19 @@ function ApiSidebarNode({ item, pathname }: { item: Node; pathname: string }) { const methodColor = methodColorMap[iconKey]; return ( - + } + > {item.name} {methodLabel && ( {methodLabel} )} - + ); } From 864e3ace827dd6505f11e3fd8456490a78096c34 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 12 May 2026 15:48:42 +0530 Subject: [PATCH 11/15] refactor: replace layout divs with Flex components - api-overview: layout, left, right, titleBlock, titleText, methodBar - api-field-list: fieldItem, expandButton, childFields - api-code-snippet: actions - api-response-panel: wrapper, container, header, tabs - playground-dialog: actionNav, splitPanel, leftPanel, panelHeader, sectionHeader, fieldRow (partial) - sidebar: apiGroup, apiGroupItems, apiItem - Remove redundant CSS layout properties handled by Flex Co-Authored-By: Claude Opus 4.6 (1M context) --- .../api/api-code-snippet.module.css | 6 --- .../src/components/api/api-code-snippet.tsx | 6 +-- .../components/api/api-field-list.module.css | 8 ---- .../src/components/api/api-field-list.tsx | 23 +++++----- .../components/api/api-overview.module.css | 28 ------------- .../src/components/api/api-overview.tsx | 30 ++++++------- .../api/api-response-panel.module.css | 17 -------- .../src/components/api/api-response-panel.tsx | 18 ++++---- .../src/components/api/playground-dialog.tsx | 42 +++++++++---------- 9 files changed, 60 insertions(+), 118 deletions(-) diff --git a/packages/chronicle/src/components/api/api-code-snippet.module.css b/packages/chronicle/src/components/api/api-code-snippet.module.css index b7c57544..1f6fd15a 100644 --- a/packages/chronicle/src/components/api/api-code-snippet.module.css +++ b/packages/chronicle/src/components/api/api-code-snippet.module.css @@ -18,12 +18,6 @@ white-space: nowrap; } -.actions { - display: flex; - align-items: center; - gap: var(--rs-space-4); -} - .body { background: var(--rs-color-background-base-primary); } diff --git a/packages/chronicle/src/components/api/api-code-snippet.tsx b/packages/chronicle/src/components/api/api-code-snippet.tsx index eeaea152..81aa497e 100644 --- a/packages/chronicle/src/components/api/api-code-snippet.tsx +++ b/packages/chronicle/src/components/api/api-code-snippet.tsx @@ -1,7 +1,7 @@ 'use client' import { useMemo, useState } from 'react' -import { CodeBlock } from '@raystack/apsara' +import { CodeBlock, Flex } from '@raystack/apsara' import { generateCurl, generatePython, @@ -42,7 +42,7 @@ export function ApiCodeSnippet({ title, method, url, headers, body }: ApiCodeSni > {title} -
+ @@ -54,7 +54,7 @@ export function ApiCodeSnippet({ title, method, url, headers, body }: ApiCodeSni -
+
{code} diff --git a/packages/chronicle/src/components/api/api-field-list.module.css b/packages/chronicle/src/components/api/api-field-list.module.css index 38f7f964..bf916457 100644 --- a/packages/chronicle/src/components/api/api-field-list.module.css +++ b/packages/chronicle/src/components/api/api-field-list.module.css @@ -7,9 +7,6 @@ } .fieldItem { - display: flex; - flex-direction: column; - gap: var(--rs-space-4); padding-bottom: var(--rs-space-5); border-bottom: 0.5px solid var(--rs-color-border-base-primary); } @@ -40,9 +37,6 @@ } .expandButton { - display: flex; - align-items: center; - justify-content: space-between; padding: var(--rs-space-3) var(--rs-space-4); border: 1px solid var(--rs-color-border-base-primary); border-radius: var(--rs-radius-2); @@ -63,8 +57,6 @@ } .childFields { - display: flex; - flex-direction: column; padding-left: var(--rs-space-5); margin-top: var(--rs-space-3); } diff --git a/packages/chronicle/src/components/api/api-field-list.tsx b/packages/chronicle/src/components/api/api-field-list.tsx index 6df922d4..7a2876c8 100644 --- a/packages/chronicle/src/components/api/api-field-list.tsx +++ b/packages/chronicle/src/components/api/api-field-list.tsx @@ -1,7 +1,7 @@ 'use client' import { useState, type ReactNode } from 'react' -import { Badge, Flex, IconButton } from '@raystack/apsara' +import { Badge, Flex } from '@raystack/apsara' import { ChevronRightIcon, ChevronDownIcon } from '@radix-ui/react-icons' import type { SchemaField } from '@/lib/schema' import styles from './api-field-list.module.css' @@ -40,18 +40,16 @@ function FieldItem({ field }: { field: SchemaField }) { const hasChildren = field.children && field.children.length > 0 return ( -
+ {field.name} {field.type} {field.description && ( - - {field.description} - + {field.description} )} {hasChildren && } -
+ ) } @@ -60,10 +58,13 @@ function ExpandableChildren({ field }: { field: SchemaField }) { return ( - + {expanded && ( -
+ {field.children!.map((child) => ( ))} -
+ )} ) diff --git a/packages/chronicle/src/components/api/api-overview.module.css b/packages/chronicle/src/components/api/api-overview.module.css index b5082942..1b3917df 100644 --- a/packages/chronicle/src/components/api/api-overview.module.css +++ b/packages/chronicle/src/components/api/api-overview.module.css @@ -1,5 +1,4 @@ .layout { - display: flex; align-items: flex-start; justify-content: space-between; padding-left: var(--rs-space-9); @@ -9,33 +8,15 @@ .left { min-width: 0; - display: flex; - flex-direction: column; - gap: var(--rs-space-10); flex: 0 1 545px; } .right { min-width: 376px; max-width: 500px; - display: flex; - flex-direction: column; - gap: var(--rs-space-8); width: 100%; } -.titleBlock { - display: flex; - flex-direction: column; - gap: var(--rs-space-7); -} - -.titleText { - display: flex; - flex-direction: column; - gap: var(--rs-space-4); -} - .title { font-family: var(--rs-font-title); font-size: var(--rs-font-size-t3); @@ -55,9 +36,6 @@ } .methodBar { - display: flex; - align-items: center; - gap: 8px; padding: var(--rs-space-3) 0; border-radius: var(--rs-radius-2); } @@ -74,12 +52,6 @@ margin: var(--rs-space-2) 0; } -.sections { - display: flex; - flex-direction: column; - gap: var(--rs-space-6); -} - @media (max-width: 1100px) { .layout { flex-direction: column; diff --git a/packages/chronicle/src/components/api/api-overview.tsx b/packages/chronicle/src/components/api/api-overview.tsx index 6b00b508..645bc4f3 100644 --- a/packages/chronicle/src/components/api/api-overview.tsx +++ b/packages/chronicle/src/components/api/api-overview.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' import type { OpenAPIV3 } from 'openapi-types' -import { Button, Menu, CopyButton, Separator } from '@raystack/apsara' +import { Flex, Button, Menu, CopyButton, Separator } from '@raystack/apsara' import { ChevronDownIcon } from '@radix-ui/react-icons' import { MethodBadge } from '@/components/api/method-badge' import { ApiCodeSnippet } from './api-code-snippet' @@ -45,26 +45,26 @@ export function ApiOverview({ method, path, operation, auth }: ApiOverviewProps) queryFields.length > 0 || (body && body.fields.length > 0) || responses.length > 0 return ( -
-
-
-
+ + + + {operation.summary && (

{operation.summary}

)} {operation.description && (

{operation.description}

)} -
-
+ + {path} -
-
+ + {hasSections && ( -
+ {authFields.length > 0 && ( )} @@ -100,11 +100,11 @@ export function ApiOverview({ method, path, operation, auth }: ApiOverviewProps) {responses.length > 0 && ( )} -
+ )} -
+ -
+ -
-
+ + ) } diff --git a/packages/chronicle/src/components/api/api-response-panel.module.css b/packages/chronicle/src/components/api/api-response-panel.module.css index ffddc8b9..18cec0c1 100644 --- a/packages/chronicle/src/components/api/api-response-panel.module.css +++ b/packages/chronicle/src/components/api/api-response-panel.module.css @@ -1,9 +1,3 @@ -.wrapper { - display: flex; - flex-direction: column; - gap: var(--rs-space-4); -} - .label { font-size: var(--rs-font-size-small); line-height: var(--rs-line-height-small); @@ -15,28 +9,17 @@ border: 0.5px solid var(--rs-color-border-base-primary); border-radius: var(--rs-radius-2); overflow: hidden; - display: flex; - flex-direction: column; height: 440px; width: 100%; } .header { - display: flex; - align-items: center; - justify-content: space-between; padding: var(--rs-space-3) var(--rs-space-5); background: var(--rs-color-background-base-secondary); border-bottom: 0.5px solid var(--rs-color-border-base-primary); flex-shrink: 0; } -.tabs { - display: flex; - align-items: center; - gap: var(--rs-space-3); -} - .tab { display: flex; align-items: center; diff --git a/packages/chronicle/src/components/api/api-response-panel.tsx b/packages/chronicle/src/components/api/api-response-panel.tsx index b700b16a..1f9650e6 100644 --- a/packages/chronicle/src/components/api/api-response-panel.tsx +++ b/packages/chronicle/src/components/api/api-response-panel.tsx @@ -1,7 +1,7 @@ 'use client' import { useState } from 'react' -import { CodeBlock, CopyButton } from '@raystack/apsara' +import { CodeBlock, CopyButton, Flex } from '@raystack/apsara' import styles from './api-response-panel.module.css' interface ResponseData { @@ -23,11 +23,11 @@ export function ApiResponsePanel({ responses }: ApiResponsePanelProps) { const displayJson = active.jsonExample ?? '{}' return ( -
+ Response: -
-
-
+ + + {responses.map((resp) => (
+ -
+
@@ -48,7 +48,7 @@ export function ApiResponsePanel({ responses }: ApiResponsePanelProps) {
-
-
+ + ) } diff --git a/packages/chronicle/src/components/api/playground-dialog.tsx b/packages/chronicle/src/components/api/playground-dialog.tsx index bea5dfe0..56ac8c23 100644 --- a/packages/chronicle/src/components/api/playground-dialog.tsx +++ b/packages/chronicle/src/components/api/playground-dialog.tsx @@ -2,7 +2,7 @@ import { useState, useCallback, useMemo } from 'react' import type { OpenAPIV3 } from 'openapi-types' -import { Dialog, Button, Badge, IconButton, InputField, CopyButton, Select, Menu } from '@raystack/apsara' +import { Dialog, Flex, Button, Badge, IconButton, InputField, CopyButton, Select, Menu } from '@raystack/apsara' import { Cross2Icon, ChevronDownIcon, ChevronUpIcon, PlayIcon, PlusIcon } from '@radix-ui/react-icons' import { CounterClockwiseClockIcon, CodeIcon } from '@radix-ui/react-icons' import { MethodBadge } from '@/components/api/method-badge' @@ -201,41 +201,41 @@ export function PlaygroundDialog({ {/* Action Nav */} -
+ {operation.summary ?? `${method} ${path}`} -
+ onOpenChange(false)} aria-label="Close"> -
-
+ + {/* Split Panel */} -
+ {/* Left Panel */} -
-
+ + Test request -
+ {/* Fields */}
{/* Auth Section */} {( <> -
+ Authorization toggleCollapse('auth')}> {collapsed.auth ? : } -
+ {!collapsed.auth && ( <> {authSchemes.length > 2 && ( -
+ Type
@@ -254,13 +253,13 @@ export function PlaygroundDialog({ )} {currentScheme.type === 'basic' ? ( <> - +
Username
setBasicUser(e.target.value)} />
- +
Password
setBasicPass(e.target.value)} /> @@ -268,7 +267,7 @@ export function PlaygroundDialog({
) : currentScheme.type !== 'none' ? ( - +
{currentScheme.headerName}
setAuthToken(e.target.value)} /> @@ -291,12 +290,12 @@ export function PlaygroundDialog({ {/* Path Params */} {pathFields.length > 0 && ( <> - +
Path Parameters toggleCollapse('path')}> {collapsed.path ? : } - +
{!collapsed.path && pathFields.map((f) => (
{f.name} @@ -316,12 +315,12 @@ export function PlaygroundDialog({ {/* Query Params */} {queryFields.length > 0 && ( <> - +
Query Parameters toggleCollapse('query')}> {collapsed.query ? : } - +
{!collapsed.query && queryFields.map((f) => (
{f.name} @@ -341,7 +340,7 @@ export function PlaygroundDialog({ {/* Body Section */} {body && ( <> - +
Body
{ @@ -358,7 +357,7 @@ export function PlaygroundDialog({ {collapsed.body ? : }
- +
{!collapsed.body && ( jsonMode ? (
From 552d79f848922f93cf4c810a534d8070842f0c20 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 12 May 2026 15:59:37 +0530 Subject: [PATCH 14/15] fix: move useState before early return in ResponseSection Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/components/api/api-overview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/chronicle/src/components/api/api-overview.tsx b/packages/chronicle/src/components/api/api-overview.tsx index dc7271b1..b92a8098 100644 --- a/packages/chronicle/src/components/api/api-overview.tsx +++ b/packages/chronicle/src/components/api/api-overview.tsx @@ -120,8 +120,8 @@ export function ApiOverview({ method, path, operation, auth }: ApiOverviewProps) } function ResponseSection({ responses }: { responses: ResponseSectionData[] }) { + const [selectedStatus, setSelectedStatus] = useState(responses[0]?.status ?? '200') if (responses.length === 0) return null - const [selectedStatus, setSelectedStatus] = useState(responses[0].status) const active = responses.find((r) => r.status === selectedStatus) ?? responses[0] return ( From 3c1e1060d107fe02600097faa2599dce1c9c00d8 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Tue, 12 May 2026 16:03:03 +0530 Subject: [PATCH 15/15] fix: address remaining CodeRabbit PR comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Handle root/empty path in deriveOperationId (fallback to 'root') - Curl snippet reflects JSON mode when body editor is active - Show error on invalid JSON body instead of silent fallback - Preserve OAuth2 flow details in Swagger 2.0 → 3.0 conversion Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/components/api/playground-dialog.tsx | 13 ++++++++++--- packages/chronicle/src/lib/api-routes.ts | 3 ++- packages/chronicle/src/lib/openapi.ts | 9 ++++++++- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/chronicle/src/components/api/playground-dialog.tsx b/packages/chronicle/src/components/api/playground-dialog.tsx index 0b33ef51..4bd1cfef 100644 --- a/packages/chronicle/src/components/api/playground-dialog.tsx +++ b/packages/chronicle/src/components/api/playground-dialog.tsx @@ -158,7 +158,13 @@ export function PlaygroundDialog({ if (body) { reqHeaders['Content-Type'] = body.contentType ?? 'application/json' if (jsonMode) { - try { reqBody = JSON.parse(bodyJsonStr) } catch { reqBody = bodyValues } + try { + reqBody = JSON.parse(bodyJsonStr) + } catch { + setResponseData({ status: 0, statusText: 'Error', body: 'Invalid JSON in request body', time: 0 }) + setLoading(false) + return + } } else { reqBody = bodyValues } @@ -193,8 +199,9 @@ export function PlaygroundDialog({ const curlSnippet = useMemo(() => { const headers: Record = { ...getAuthHeaders(), ...headerValues } if (body) headers['Content-Type'] = body.contentType ?? 'application/json' - return generateCurl({ method, url: serverUrl + path, headers, body: body ? JSON.stringify(bodyValues) : undefined }) - }, [method, path, serverUrl, getAuthHeaders, headerValues, bodyValues, body]) + const bodyStr = body ? (jsonMode ? bodyJsonStr : JSON.stringify(bodyValues)) : undefined + return generateCurl({ method, url: serverUrl + path, headers, body: bodyStr }) + }, [method, path, serverUrl, getAuthHeaders, headerValues, bodyValues, bodyJsonStr, jsonMode, body]) return ( diff --git a/packages/chronicle/src/lib/api-routes.ts b/packages/chronicle/src/lib/api-routes.ts index 459aedba..bed43141 100644 --- a/packages/chronicle/src/lib/api-routes.ts +++ b/packages/chronicle/src/lib/api-routes.ts @@ -8,7 +8,8 @@ export function getSpecSlug(spec: ApiSpec): string { } function deriveOperationId(method: string, path: string): string { - return `${method}_${path.replace(/[/{}\-]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '')}` + const slug = path.replace(/[/{}\-]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '') + return `${method}_${slug || 'root'}` } function getOperationId(op: OpenAPIV3.OperationObject, method: string, path: string): string { diff --git a/packages/chronicle/src/lib/openapi.ts b/packages/chronicle/src/lib/openapi.ts index 379c84a3..ef9183db 100644 --- a/packages/chronicle/src/lib/openapi.ts +++ b/packages/chronicle/src/lib/openapi.ts @@ -141,7 +141,14 @@ function convertV2SecurityDefs(defs: Record } + const flow = { authorizationUrl: v2.authorizationUrl ?? '', tokenUrl: v2.tokenUrl ?? '', scopes: v2.scopes ?? {} } + const flows: OpenAPIV3.OAuth2SecurityScheme['flows'] = {} + if (v2.flow === 'implicit') flows.implicit = { authorizationUrl: flow.authorizationUrl, scopes: flow.scopes } + else if (v2.flow === 'password') flows.password = { tokenUrl: flow.tokenUrl, scopes: flow.scopes } + else if (v2.flow === 'application') flows.clientCredentials = { tokenUrl: flow.tokenUrl, scopes: flow.scopes } + else if (v2.flow === 'accessCode') flows.authorizationCode = { authorizationUrl: flow.authorizationUrl, tokenUrl: flow.tokenUrl, scopes: flow.scopes } + result[name] = { type: 'oauth2', flows } as OpenAPIV3.OAuth2SecurityScheme } } return result