raffel 1.1.48 → 1.1.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1214,60 +1214,247 @@ function generateExampleFromSchema(schema) {
1214
1214
  default: return null;
1215
1215
  }
1216
1216
  }
1217
+ function httpBaseUrl() {
1218
+ return String(spec?.servers?.[0]?.url ?? 'http://localhost:3000').replace(/\/$/, '');
1219
+ }
1220
+ // Collect JSON-Schema validation constraints as readable chips.
1221
+ function schemaConstraintChips(schema) {
1222
+ if (!schema || typeof schema !== 'object')
1223
+ return [];
1224
+ const chips = [];
1225
+ if (schema.format)
1226
+ chips.push(`format: ${schema.format}`);
1227
+ if (typeof schema.minLength === 'number')
1228
+ chips.push(`min length: ${schema.minLength}`);
1229
+ if (typeof schema.maxLength === 'number')
1230
+ chips.push(`max length: ${schema.maxLength}`);
1231
+ if (typeof schema.minimum === 'number')
1232
+ chips.push(`>= ${schema.minimum}`);
1233
+ if (typeof schema.exclusiveMinimum === 'number')
1234
+ chips.push(`> ${schema.exclusiveMinimum}`);
1235
+ if (typeof schema.maximum === 'number')
1236
+ chips.push(`<= ${schema.maximum}`);
1237
+ if (typeof schema.exclusiveMaximum === 'number')
1238
+ chips.push(`< ${schema.exclusiveMaximum}`);
1239
+ if (typeof schema.multipleOf === 'number')
1240
+ chips.push(`multiple of ${schema.multipleOf}`);
1241
+ if (typeof schema.minItems === 'number')
1242
+ chips.push(`min items: ${schema.minItems}`);
1243
+ if (typeof schema.maxItems === 'number')
1244
+ chips.push(`max items: ${schema.maxItems}`);
1245
+ if (schema.uniqueItems)
1246
+ chips.push('unique items');
1247
+ if (typeof schema.pattern === 'string')
1248
+ chips.push(`pattern: ${schema.pattern}`);
1249
+ if (schema.nullable)
1250
+ chips.push('nullable');
1251
+ if (schema.readOnly)
1252
+ chips.push('read-only');
1253
+ if (schema.writeOnly)
1254
+ chips.push('write-only');
1255
+ return chips;
1256
+ }
1257
+ // Append constraint / default / enum chips to a row element.
1258
+ function appendConstraintChips(row, schema) {
1259
+ if (!schema || typeof schema !== 'object')
1260
+ return;
1261
+ const chips = schemaConstraintChips(schema);
1262
+ const hasDefault = schema.default !== undefined;
1263
+ const enumValues = Array.isArray(schema.enum) ? schema.enum : [];
1264
+ if (!chips.length && !hasDefault && enumValues.length === 0)
1265
+ return;
1266
+ const wrap = doc.createElement('div');
1267
+ wrap.className = 'constraint-chips';
1268
+ if (hasDefault) {
1269
+ const chip = doc.createElement('span');
1270
+ chip.className = 'constraint-chip';
1271
+ chip.textContent = `default: ${JSON.stringify(schema.default)}`;
1272
+ wrap.appendChild(chip);
1273
+ }
1274
+ chips.forEach(text => {
1275
+ const chip = doc.createElement('span');
1276
+ chip.className = 'constraint-chip';
1277
+ chip.textContent = text;
1278
+ wrap.appendChild(chip);
1279
+ });
1280
+ enumValues.forEach(value => {
1281
+ const chip = doc.createElement('span');
1282
+ chip.className = 'constraint-chip constraint-enum';
1283
+ chip.textContent = String(value);
1284
+ wrap.appendChild(chip);
1285
+ });
1286
+ row.appendChild(wrap);
1287
+ }
1288
+ // Render a JSON value as a Python literal (for the Python request sample).
1289
+ function pyLiteral(value) {
1290
+ if (value === null || value === undefined)
1291
+ return 'None';
1292
+ if (value === true)
1293
+ return 'True';
1294
+ if (value === false)
1295
+ return 'False';
1296
+ if (typeof value === 'number')
1297
+ return String(value);
1298
+ if (typeof value === 'string')
1299
+ return JSON.stringify(value);
1300
+ if (Array.isArray(value))
1301
+ return `[${value.map(pyLiteral).join(', ')}]`;
1302
+ if (typeof value === 'object') {
1303
+ return `{${Object.entries(value).map(([k, v]) => `${JSON.stringify(k)}: ${pyLiteral(v)}`).join(', ')}}`;
1304
+ }
1305
+ return 'None';
1306
+ }
1307
+ // Build request samples in cURL, JavaScript, Python and Rust.
1308
+ function buildHttpSamples(method, url, body) {
1309
+ const hasBody = body !== null && body !== undefined && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method);
1310
+ const jsonPretty = hasBody ? JSON.stringify(body, null, 2) : '';
1311
+ const samples = {};
1312
+ let curl = `curl -X ${method} "${url}"`;
1313
+ if (hasBody)
1314
+ curl += ` \\\n -H "Content-Type: application/json" \\\n -d '${JSON.stringify(body)}'`;
1315
+ samples.curl = curl;
1316
+ let js = `const res = await fetch(${JSON.stringify(url)}, {\n method: ${JSON.stringify(method)},`;
1317
+ if (hasBody)
1318
+ js += `\n headers: { "Content-Type": "application/json" },\n body: JSON.stringify(${jsonPretty.split('\n').join('\n ')}),`;
1319
+ js += `\n});\nconst data = await res.json();\nconsole.log(data);`;
1320
+ samples.javascript = js;
1321
+ let py = `import requests\n\nres = requests.${method.toLowerCase()}(\n ${JSON.stringify(url)},`;
1322
+ if (hasBody)
1323
+ py += `\n json=${pyLiteral(body)},`;
1324
+ py += `\n)\nprint(res.json())`;
1325
+ samples.python = py;
1326
+ let rust = `use reqwest::Client;\n\nlet client = Client::new();\nlet res = client\n .${method.toLowerCase()}(${JSON.stringify(url)})`;
1327
+ if (hasBody)
1328
+ rust += `\n .json(&serde_json::json!(${jsonPretty.split('\n').join('\n ')}))`;
1329
+ rust += `\n .send()\n .await?;\nlet body = res.text().await?;\nprintln!("{}", body);`;
1330
+ samples.rust = rust;
1331
+ return samples;
1332
+ }
1217
1333
  function renderCodeExamples(endpoint, data) {
1218
- const container = doc.createElement('div');
1219
- container.style.marginTop = '20px';
1220
- container.style.paddingTop = '16px';
1221
- container.style.borderTop = '1px solid var(--border-color)';
1222
- // cURL example
1223
- const method = endpoint.method?.toUpperCase() || 'GET';
1224
- const path = endpoint.path || '/';
1225
- const baseUrl = 'http://localhost:3000';
1226
- const curlSection = doc.createElement('div');
1227
- curlSection.style.marginBottom = '20px';
1228
- curlSection.innerHTML = '<div style="font-size: 12px; font-weight: 600; color: var(--text-secondary); margin-bottom: 8px;">cURL EXAMPLE</div>';
1229
- const curlCode = doc.createElement('pre');
1230
- curlCode.style.backgroundColor = 'var(--bg-secondary)';
1231
- curlCode.style.padding = '12px';
1232
- curlCode.style.borderRadius = '4px';
1233
- curlCode.style.fontSize = '12px';
1234
- curlCode.style.overflow = 'auto';
1235
- let curlCmd = `curl -X ${method} "${baseUrl}${path}"`;
1236
- const reqBody = data.requestBody?.content?.[Object.keys(data.requestBody.content)[0]];
1237
- if (reqBody && ['POST', 'PUT', 'PATCH'].includes(method)) {
1238
- const example = generateExampleFromSchema(reqBody.schema);
1239
- curlCmd += ` \\\n -H "Content-Type: application/json" \\\n -d '${JSON.stringify(example, null, 2).split('\n').join('\n ')}'`;
1240
- }
1241
- curlCode.textContent = curlCmd;
1242
- curlSection.appendChild(curlCode);
1243
- container.appendChild(curlSection);
1244
- // Response example
1245
- if (data.responses) {
1246
- const responses = Object.entries(data.responses);
1247
- if (responses.length > 0) {
1248
- const [status, resp] = responses[0];
1249
- const respSection = doc.createElement('div');
1250
- respSection.innerHTML = `<div style="font-size: 12px; font-weight: 600; color: var(--text-secondary); margin-bottom: 8px;">RESPONSE ${status}</div>`;
1251
- const respCode = doc.createElement('pre');
1252
- respCode.style.backgroundColor = 'var(--bg-secondary)';
1253
- respCode.style.padding = '12px';
1254
- respCode.style.borderRadius = '4px';
1255
- respCode.style.fontSize = '12px';
1256
- respCode.style.overflow = 'auto';
1257
- if (resp.content) {
1258
- const contentType = Object.keys(resp.content)[0];
1259
- const schema = resp.content[contentType]?.schema;
1260
- const example = generateExampleFromSchema(schema);
1261
- respCode.textContent = JSON.stringify(example, null, 2);
1262
- }
1263
- else {
1264
- respCode.textContent = `{}`;
1334
+ const method = (endpoint.method || 'GET').toUpperCase();
1335
+ const url = `${httpBaseUrl()}${endpoint.path || '/'}`;
1336
+ const reqContent = data.requestBody?.content;
1337
+ const reqSchema = reqContent ? reqContent[Object.keys(reqContent)[0]]?.schema : null;
1338
+ const bodyExample = reqSchema ? generateExampleFromSchema(reqSchema) : null;
1339
+ const samples = buildHttpSamples(method, url, bodyExample);
1340
+ const langs = [['curl', 'cURL'], ['javascript', 'JavaScript'], ['python', 'Python'], ['rust', 'Rust']];
1341
+ const wrap = doc.createElement('div');
1342
+ wrap.className = 'http-code-samples';
1343
+ const tabs = doc.createElement('div');
1344
+ tabs.className = 'code-tabs';
1345
+ const contents = doc.createElement('div');
1346
+ contents.className = 'code-contents';
1347
+ langs.forEach(([key, label], index) => {
1348
+ const tab = doc.createElement('button');
1349
+ tab.type = 'button';
1350
+ tab.className = `code-tab${index === 0 ? ' active' : ''}`;
1351
+ tab.textContent = label;
1352
+ const content = doc.createElement('div');
1353
+ content.className = `code-content${index === 0 ? ' active' : ''}`;
1354
+ const pre = doc.createElement('pre');
1355
+ pre.className = 'http-code-sample-pre';
1356
+ pre.textContent = samples[key];
1357
+ content.appendChild(pre);
1358
+ tab.onclick = () => {
1359
+ tabs.querySelectorAll('.code-tab').forEach((t) => t.classList.remove('active'));
1360
+ contents.querySelectorAll('.code-content').forEach((c) => c.classList.remove('active'));
1361
+ tab.classList.add('active');
1362
+ content.classList.add('active');
1363
+ };
1364
+ tabs.appendChild(tab);
1365
+ contents.appendChild(content);
1366
+ });
1367
+ const copy = doc.createElement('button');
1368
+ copy.type = 'button';
1369
+ copy.className = 'http-code-copy';
1370
+ copy.textContent = 'Copy';
1371
+ copy.onclick = () => {
1372
+ const active = contents.querySelector('.code-content.active pre');
1373
+ const text = active?.textContent ?? '';
1374
+ const clip = globalThis.navigator?.clipboard;
1375
+ if (clip?.writeText) {
1376
+ clip.writeText(text).then(() => {
1377
+ copy.textContent = 'Copied';
1378
+ setTimeout(() => { copy.textContent = 'Copy'; }, 1200);
1379
+ });
1380
+ }
1381
+ };
1382
+ tabs.appendChild(copy);
1383
+ wrap.appendChild(tabs);
1384
+ wrap.appendChild(contents);
1385
+ return wrap;
1386
+ }
1387
+ // Append a JSON example block to a container.
1388
+ // Syntax-highlight a JSON value into HTML using the .sample-json token classes.
1389
+ function highlightJsonHtml(value) {
1390
+ let json = JSON.stringify(value, null, 2);
1391
+ if (json === undefined)
1392
+ return '';
1393
+ json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
1394
+ return json.replace(/("(?:\\.|[^"\\])*"(\s*:)?|\b(?:true|false|null)\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g, (match, _token, colon) => {
1395
+ let cls = 'json-number';
1396
+ if (match[0] === '"')
1397
+ cls = colon ? 'json-key' : 'json-string';
1398
+ else if (match === 'true' || match === 'false')
1399
+ cls = 'json-boolean';
1400
+ else if (match === 'null')
1401
+ cls = 'json-null';
1402
+ return `<span class="${cls}">${match}</span>`;
1403
+ });
1404
+ }
1405
+ // Right-panel response samples: one status tab per response, each showing the
1406
+ // generated JSON example (colorized) — ReDoc's response sample switcher.
1407
+ function renderResponseSamples(responses) {
1408
+ const wrap = doc.createElement('div');
1409
+ const tabs = doc.createElement('div');
1410
+ tabs.className = 'sample-tabs';
1411
+ const contents = doc.createElement('div');
1412
+ contents.className = 'sample-contents';
1413
+ const entries = Object.entries(responses);
1414
+ entries.forEach(([status, resp], index) => {
1415
+ const statusClass = status.startsWith('2') ? 'status-2xx'
1416
+ : status.startsWith('4') ? 'status-4xx'
1417
+ : status.startsWith('5') ? 'status-5xx'
1418
+ : '';
1419
+ const tab = doc.createElement('button');
1420
+ tab.type = 'button';
1421
+ tab.className = `sample-tab ${statusClass}${index === 0 ? ' active' : ''}`;
1422
+ tab.textContent = status;
1423
+ const content = doc.createElement('div');
1424
+ content.className = `sample-content${index === 0 ? ' active' : ''}`;
1425
+ const ct = resp.content ? Object.keys(resp.content)[0] : null;
1426
+ const schema = ct ? resp.content[ct]?.schema : null;
1427
+ const example = schema ? generateExampleFromSchema(schema) : null;
1428
+ if (example !== null && example !== undefined) {
1429
+ if (ct) {
1430
+ const typeLine = doc.createElement('div');
1431
+ typeLine.className = 'sample-content-type';
1432
+ typeLine.textContent = ct;
1433
+ content.appendChild(typeLine);
1265
1434
  }
1266
- respSection.appendChild(respCode);
1267
- container.appendChild(respSection);
1435
+ const pre = doc.createElement('pre');
1436
+ pre.className = 'sample-json';
1437
+ pre.innerHTML = highlightJsonHtml(example);
1438
+ content.appendChild(pre);
1268
1439
  }
1269
- }
1270
- return container;
1440
+ else {
1441
+ const empty = doc.createElement('div');
1442
+ empty.className = 'no-example';
1443
+ empty.textContent = resp.description || 'No response body.';
1444
+ content.appendChild(empty);
1445
+ }
1446
+ tab.onclick = () => {
1447
+ tabs.querySelectorAll('.sample-tab').forEach((t) => t.classList.remove('active'));
1448
+ contents.querySelectorAll('.sample-content').forEach((c) => c.classList.remove('active'));
1449
+ tab.classList.add('active');
1450
+ content.classList.add('active');
1451
+ };
1452
+ tabs.appendChild(tab);
1453
+ contents.appendChild(content);
1454
+ });
1455
+ wrap.appendChild(tabs);
1456
+ wrap.appendChild(contents);
1457
+ return wrap;
1271
1458
  }
1272
1459
  function renderSchemaTree(parent, schema, depth = 0) {
1273
1460
  if (!schema)
@@ -1290,8 +1477,7 @@ function renderSchemaTree(parent, schema, depth = 0) {
1290
1477
  row.style.paddingLeft = depth === 0 ? '8px' : '0';
1291
1478
  const type = prop.type || 'any';
1292
1479
  const required = schema.required?.includes(key) ? '<span style="color: #ef4444; margin-left: 4px;">*</span>' : '';
1293
- const format = prop.format ? ` (${prop.format})` : '';
1294
- row.innerHTML = `<div style="font-weight: 500; color: var(--text-primary); display: flex; align-items: center;">${esc(key)}${required} <span style="color: var(--text-secondary); font-weight: normal; margin-left: 8px;">${type}${format}</span></div>`;
1480
+ row.innerHTML = `<div style="font-weight: 500; color: var(--text-primary); display: flex; align-items: center;">${esc(key)}${required} <span class="schema-type type-${esc(prop.type || 'null')}" style="margin-left: 8px;">${esc(type)}</span></div>`;
1295
1481
  if (prop.description) {
1296
1482
  const desc = doc.createElement('div');
1297
1483
  desc.style.fontSize = '12px';
@@ -1300,33 +1486,20 @@ function renderSchemaTree(parent, schema, depth = 0) {
1300
1486
  desc.textContent = prop.description;
1301
1487
  row.appendChild(desc);
1302
1488
  }
1303
- if (prop.example !== undefined) {
1304
- const ex = doc.createElement('div');
1305
- ex.style.fontSize = '11px';
1306
- ex.style.color = '#888';
1307
- ex.style.marginTop = '2px';
1308
- ex.style.fontFamily = 'monospace';
1309
- ex.textContent = `Example: ${JSON.stringify(prop.example)}`;
1310
- row.appendChild(ex);
1311
- }
1312
- if (prop.enum) {
1313
- const en = doc.createElement('div');
1314
- en.style.fontSize = '11px';
1315
- en.style.color = '#888';
1316
- en.style.marginTop = '2px';
1317
- en.textContent = `Values: ${prop.enum.join(', ')}`;
1318
- row.appendChild(en);
1319
- }
1489
+ appendConstraintChips(row, prop);
1320
1490
  div.appendChild(row);
1321
- if (prop.properties) {
1491
+ if (prop.type === 'object' && prop.properties) {
1322
1492
  renderSchemaTree(div, prop, depth + 1);
1323
1493
  }
1494
+ else if (prop.type === 'array' && prop.items?.properties) {
1495
+ renderSchemaTree(div, prop.items, depth + 1);
1496
+ }
1324
1497
  });
1325
1498
  }
1326
1499
  else if (schema.type === 'array' && schema.items) {
1327
1500
  const row = doc.createElement('div');
1328
1501
  row.style.padding = '6px 0';
1329
- row.innerHTML = `<span style="color: var(--text-primary); font-weight: 500;">array</span> <span style="color: var(--text-secondary);">of ${schema.items.type || 'object'}</span>`;
1502
+ row.innerHTML = `<span style="color: var(--text-primary); font-weight: 500;">array</span> <span style="color: var(--text-secondary);">of ${esc(schema.items.type || 'object')}</span>`;
1330
1503
  div.appendChild(row);
1331
1504
  if (schema.items.properties) {
1332
1505
  renderSchemaTree(div, schema.items, depth + 1);
@@ -1336,10 +1509,107 @@ function renderSchemaTree(parent, schema, depth = 0) {
1336
1509
  const row = doc.createElement('div');
1337
1510
  row.style.padding = '6px 0';
1338
1511
  row.innerHTML = `<span style="color: var(--text-secondary);">${esc(schema.type || 'any')}</span>`;
1512
+ appendConstraintChips(row, schema);
1339
1513
  div.appendChild(row);
1340
1514
  }
1341
1515
  parent.appendChild(div);
1342
1516
  }
1517
+ // Render one HTTP parameter group (path/query/header/cookie) ReDoc-style.
1518
+ function renderParamGroup(title, params) {
1519
+ const group = doc.createElement('div');
1520
+ group.className = 'http-param-group';
1521
+ const heading = doc.createElement('div');
1522
+ heading.className = 'http-param-group-title';
1523
+ heading.textContent = title;
1524
+ group.appendChild(heading);
1525
+ const list = doc.createElement('div');
1526
+ list.className = 'http-params';
1527
+ params.forEach(param => {
1528
+ const item = doc.createElement('div');
1529
+ item.className = 'http-param';
1530
+ const schema = param.schema || {};
1531
+ const typeName = schema.type === 'array' ? `${schema.items?.type || 'any'}[]` : (schema.type || 'string');
1532
+ const head = doc.createElement('div');
1533
+ head.className = 'http-param-head';
1534
+ head.innerHTML = `<span class="http-param-name">${esc(param.name)}</span>` +
1535
+ `<span class="schema-type type-${esc(schema.type || 'string')}">${esc(typeName)}</span>` +
1536
+ (param.required ? '<span class="http-param-required">required</span>' : '') +
1537
+ (param.deprecated ? '<span class="http-param-deprecated">deprecated</span>' : '');
1538
+ item.appendChild(head);
1539
+ const description = param.description || schema.description;
1540
+ if (description) {
1541
+ const desc = doc.createElement('div');
1542
+ desc.className = 'http-param-desc';
1543
+ desc.textContent = description;
1544
+ item.appendChild(desc);
1545
+ }
1546
+ appendConstraintChips(item, schema);
1547
+ list.appendChild(item);
1548
+ });
1549
+ group.appendChild(list);
1550
+ return group;
1551
+ }
1552
+ // Render a single response as a collapsible accordion (ReDoc-style).
1553
+ function renderResponseAccordion(status, resp, openByDefault) {
1554
+ const statusClass = status.startsWith('2') ? 'status-2xx'
1555
+ : status.startsWith('3') ? 'status-3xx'
1556
+ : status.startsWith('4') ? 'status-4xx'
1557
+ : 'status-5xx';
1558
+ const acc = doc.createElement('div');
1559
+ acc.className = `response-accordion${openByDefault ? ' open' : ''}`;
1560
+ const header = doc.createElement('button');
1561
+ header.type = 'button';
1562
+ header.className = 'response-accordion-header';
1563
+ header.innerHTML = `<span class="response-accordion-caret">▶</span>` +
1564
+ `<span class="response-status-dot ${statusClass}"></span>` +
1565
+ `<span class="response-status-code">${esc(status)}</span>` +
1566
+ `<span class="response-status-desc">${esc(resp.description || '')}</span>`;
1567
+ const body = doc.createElement('div');
1568
+ body.className = 'response-accordion-body';
1569
+ if (resp.headers && Object.keys(resp.headers).length > 0) {
1570
+ const block = doc.createElement('div');
1571
+ block.className = 'response-block';
1572
+ const sub = doc.createElement('div');
1573
+ sub.className = 'response-subhead';
1574
+ sub.textContent = 'Response Headers';
1575
+ block.appendChild(sub);
1576
+ const list = doc.createElement('div');
1577
+ list.className = 'http-params';
1578
+ Object.entries(resp.headers).forEach(([name, def]) => {
1579
+ const item = doc.createElement('div');
1580
+ item.className = 'http-param';
1581
+ const hschema = def.schema || {};
1582
+ item.innerHTML = `<div class="http-param-head"><span class="http-param-name">${esc(name)}</span><span class="schema-type type-${esc(hschema.type || 'string')}">${esc(hschema.type || 'string')}</span></div>` +
1583
+ (def.description ? `<div class="http-param-desc">${esc(def.description)}</div>` : '');
1584
+ list.appendChild(item);
1585
+ });
1586
+ block.appendChild(list);
1587
+ body.appendChild(block);
1588
+ }
1589
+ const content = resp.content;
1590
+ const contentType = content ? Object.keys(content)[0] : null;
1591
+ const schema = contentType ? content[contentType]?.schema : null;
1592
+ if (schema) {
1593
+ const block = doc.createElement('div');
1594
+ block.className = 'response-block';
1595
+ const sub = doc.createElement('div');
1596
+ sub.className = 'response-subhead';
1597
+ sub.textContent = `Response Body${contentType ? ` · ${contentType}` : ''}`;
1598
+ block.appendChild(sub);
1599
+ renderSchemaTree(block, schema);
1600
+ body.appendChild(block);
1601
+ }
1602
+ if (!body.children.length) {
1603
+ const empty = doc.createElement('div');
1604
+ empty.className = 'response-desc-only';
1605
+ empty.textContent = resp.description || 'No content.';
1606
+ body.appendChild(empty);
1607
+ }
1608
+ header.onclick = () => acc.classList.toggle('open');
1609
+ acc.appendChild(header);
1610
+ acc.appendChild(body);
1611
+ return acc;
1612
+ }
1343
1613
  function renderEndpointDetails(endpoint) {
1344
1614
  const container = doc.createElement('div');
1345
1615
  container.className = 'endpoint-details';
@@ -1347,82 +1617,81 @@ function renderEndpointDetails(endpoint) {
1347
1617
  appendProtocolConsole(container, { doc, spec, wsSpec, streamsSpec, jsonrpcSpec, activeProtocol, endpoint, data, esc, escapeAttr });
1348
1618
  const appendMany = (items) => items.forEach(([title, value]) => appendSchemaSubsection(container, title, value));
1349
1619
  if (activeProtocol === 'http') {
1620
+ // Two-column operation layout (ReDoc-style): the left column carries the
1621
+ // contract (params + schemas + responses); the right column is a sticky
1622
+ // dark panel with the request samples and response examples.
1623
+ const content = doc.createElement('div');
1624
+ content.className = 'endpoint-content';
1625
+ const left = doc.createElement('div');
1626
+ left.className = 'endpoint-left';
1627
+ const right = doc.createElement('div');
1628
+ right.className = 'endpoint-right';
1350
1629
  const params = (data.parameters ?? []);
1351
1630
  if (params.length > 0) {
1352
1631
  const section = doc.createElement('div');
1353
1632
  section.className = 'endpoint-subsection';
1354
1633
  section.innerHTML = '<div class="subsection-label">PARAMETERS</div>';
1355
- const grid = doc.createElement('div');
1356
- grid.className = 'info-grid';
1357
- params.forEach(p => {
1358
- grid.innerHTML += `<div class="info-card"><div class="info-card-title">${esc(p.name)}</div><div class="info-card-value">${esc(p.in || 'query')} • ${esc(p.schema?.type || 'string')}${p.required ? ' *' : ''}</div></div>`;
1634
+ const groups = [
1635
+ ['path', 'Path Parameters'],
1636
+ ['query', 'Query Parameters'],
1637
+ ['header', 'Header Parameters'],
1638
+ ['cookie', 'Cookie Parameters'],
1639
+ ];
1640
+ groups.forEach(([location, title]) => {
1641
+ const inGroup = params.filter(p => (p.in || 'query') === location);
1642
+ if (inGroup.length > 0)
1643
+ section.appendChild(renderParamGroup(title, inGroup));
1359
1644
  });
1360
- section.appendChild(grid);
1361
- container.appendChild(section);
1645
+ left.appendChild(section);
1362
1646
  }
1363
1647
  const reqBody = data.requestBody;
1364
1648
  if (reqBody?.content) {
1365
1649
  const section = doc.createElement('div');
1366
1650
  section.className = 'endpoint-subsection';
1367
- section.innerHTML = '<div class="subsection-label">REQUEST BODY</div>';
1368
- const content = reqBody.content[Object.keys(reqBody.content)[0]];
1369
- if (content?.schema) {
1370
- renderSchemaTree(section, content.schema);
1371
- }
1372
- container.appendChild(section);
1651
+ const contentType = Object.keys(reqBody.content)[0];
1652
+ section.innerHTML = `<div class="subsection-label">REQUEST BODY${reqBody.required ? ' <span style="color:#ef4444">required</span>' : ''}${contentType ? ` · ${esc(contentType)}` : ''}</div>`;
1653
+ const bodyContent = reqBody.content[contentType];
1654
+ if (bodyContent?.schema)
1655
+ renderSchemaTree(section, bodyContent.schema);
1656
+ left.appendChild(section);
1373
1657
  }
1374
1658
  const responses = data.responses;
1375
1659
  if (responses && Object.keys(responses).length > 0) {
1376
1660
  const section = doc.createElement('div');
1377
1661
  section.className = 'endpoint-subsection';
1378
1662
  section.innerHTML = '<div class="subsection-label">RESPONSES</div>';
1379
- Object.entries(responses).forEach(([status, resp]) => {
1380
- const statusClass = status.startsWith('2') ? 'status-2xx' : status.startsWith('4') ? 'status-4xx' : 'status-5xx';
1381
- const item = doc.createElement('div');
1382
- item.style.marginBottom = '16px';
1383
- item.style.padding = '12px';
1384
- item.style.borderRadius = '6px';
1385
- item.style.backgroundColor = 'var(--bg-secondary)';
1386
- item.innerHTML = `<div style="color: var(--text-secondary); font-size: 14px; margin-bottom: 12px; display: flex; align-items: center; gap: 8px;"><span class="badge badge-${statusClass}">${status}</span> <strong>${esc(resp.description || '')}</strong></div>`;
1387
- // Headers
1388
- if (resp.headers && Object.keys(resp.headers).length > 0) {
1389
- const headersDiv = doc.createElement('div');
1390
- headersDiv.style.marginBottom = '12px';
1391
- headersDiv.innerHTML = '<div style="font-size: 12px; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px;">HEADERS</div>';
1392
- const headersList = doc.createElement('div');
1393
- headersList.style.fontSize = '12px';
1394
- Object.entries(resp.headers).forEach(([headerName, headerDef]) => {
1395
- const headerRow = doc.createElement('div');
1396
- headerRow.style.padding = '4px 0';
1397
- headerRow.style.borderLeft = '2px solid var(--border-color)';
1398
- headerRow.style.paddingLeft = '8px';
1399
- const headerType = headerDef.schema?.type || 'string';
1400
- headerRow.innerHTML = `<div style="font-weight: 500; color: var(--text-primary);">${esc(headerName)}</div><div style="color: var(--text-muted); font-size: 11px;">${headerType}</div>${headerDef.description ? `<div style="color: var(--text-muted); font-size: 11px; margin-top: 2px;">${esc(headerDef.description)}</div>` : ''}`;
1401
- headersList.appendChild(headerRow);
1402
- });
1403
- headersDiv.appendChild(headersList);
1404
- item.appendChild(headersDiv);
1405
- }
1406
- // Schema
1407
- if (resp.content) {
1408
- const contentType = Object.keys(resp.content)[0];
1409
- const schema = resp.content[contentType]?.schema;
1410
- if (schema) {
1411
- const schemaDiv = doc.createElement('div');
1412
- schemaDiv.innerHTML = '<div style="font-size: 12px; font-weight: 600; color: var(--text-secondary); margin-bottom: 6px;">SCHEMA</div>';
1413
- renderSchemaTree(schemaDiv, schema);
1414
- item.appendChild(schemaDiv);
1415
- }
1416
- }
1417
- section.appendChild(item);
1663
+ const entries = Object.entries(responses);
1664
+ let opened = false;
1665
+ entries.forEach(([status, resp]) => {
1666
+ const open = !opened && status.startsWith('2');
1667
+ if (open)
1668
+ opened = true;
1669
+ section.appendChild(renderResponseAccordion(status, resp, open));
1418
1670
  });
1419
- container.appendChild(section);
1671
+ if (!opened) {
1672
+ const first = section.querySelector('.response-accordion');
1673
+ if (first)
1674
+ first.classList.add('open');
1675
+ }
1676
+ left.appendChild(section);
1677
+ }
1678
+ // Right column: request samples (cURL / JavaScript / Python / Rust).
1679
+ const samplesSection = doc.createElement('div');
1680
+ samplesSection.className = 'endpoint-right-section';
1681
+ samplesSection.innerHTML = '<div class="endpoint-right-header">Request samples</div>';
1682
+ samplesSection.appendChild(renderCodeExamples(endpoint, data));
1683
+ right.appendChild(samplesSection);
1684
+ // Right column: response samples (JSON examples per status code).
1685
+ if (responses && Object.keys(responses).length > 0) {
1686
+ const respSamples = doc.createElement('div');
1687
+ respSamples.className = 'endpoint-right-section';
1688
+ respSamples.innerHTML = '<div class="endpoint-right-header">Response samples</div>';
1689
+ respSamples.appendChild(renderResponseSamples(responses));
1690
+ right.appendChild(respSamples);
1420
1691
  }
1421
- // Code examples
1422
- const examplesSection = doc.createElement('div');
1423
- examplesSection.className = 'endpoint-subsection';
1424
- examplesSection.appendChild(renderCodeExamples(endpoint, data));
1425
- container.appendChild(examplesSection);
1692
+ content.appendChild(left);
1693
+ content.appendChild(right);
1694
+ container.appendChild(content);
1426
1695
  }
1427
1696
  if (activeProtocol === 'websocket') {
1428
1697
  appendInfoGrid(container, [['Channel Type', data.type], ['Path', endpoint.path]]);
@@ -1705,23 +1974,35 @@ function pageNavCard(entry, label, direction) {
1705
1974
  button.onclick = () => setDocsPage(entry.path);
1706
1975
  return button;
1707
1976
  }
1977
+ function setTocColumn(visible) {
1978
+ const shell = doc.querySelector?.('.main-shell');
1979
+ if (shell)
1980
+ shell.classList.toggle('main-shell-no-toc', !visible);
1981
+ }
1708
1982
  function renderToc(root) {
1709
1983
  const toc = byId('pageToc');
1710
1984
  if (!toc)
1711
1985
  return;
1712
1986
  toc.textContent = '';
1713
- if (tocConfig.enabled === false)
1987
+ if (tocConfig.enabled === false) {
1988
+ setTocColumn(false);
1714
1989
  return;
1715
- if (root.querySelector?.('[data-markdown-ignore-all="true"]'))
1990
+ }
1991
+ if (root.querySelector?.('[data-markdown-ignore-all="true"]')) {
1992
+ setTocColumn(false);
1716
1993
  return;
1994
+ }
1717
1995
  const min = Number(tocConfig.minLevel ?? 2);
1718
1996
  const max = Number(tocConfig.maxLevel ?? 3);
1719
1997
  const headings = Array.from(root.querySelectorAll('h1,h2,h3,h4,h5,h6') ?? []).filter((heading) => {
1720
1998
  const level = Number(heading.tagName.slice(1));
1721
1999
  return heading.id && level >= min && level <= max && heading.dataset?.markdownIgnore !== 'true';
1722
2000
  });
1723
- if (headings.length === 0)
2001
+ if (headings.length === 0) {
2002
+ setTocColumn(false);
1724
2003
  return;
2004
+ }
2005
+ setTocColumn(true);
1725
2006
  const title = doc.createElement('div');
1726
2007
  title.className = 'toc-title';
1727
2008
  title.textContent = 'On this page';