vector-framework 1.2.0 → 1.2.2

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.
Files changed (77) hide show
  1. package/README.md +19 -0
  2. package/dist/auth/protected.d.ts +1 -0
  3. package/dist/auth/protected.d.ts.map +1 -1
  4. package/dist/auth/protected.js +3 -0
  5. package/dist/auth/protected.js.map +1 -1
  6. package/dist/cache/manager.d.ts +1 -0
  7. package/dist/cache/manager.d.ts.map +1 -1
  8. package/dist/cache/manager.js +3 -0
  9. package/dist/cache/manager.js.map +1 -1
  10. package/dist/cli/graceful-shutdown.d.ts +15 -0
  11. package/dist/cli/graceful-shutdown.d.ts.map +1 -0
  12. package/dist/cli/graceful-shutdown.js +42 -0
  13. package/dist/cli/graceful-shutdown.js.map +1 -0
  14. package/dist/cli/index.js +37 -43
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/cli.js +967 -222
  17. package/dist/core/config-loader.d.ts.map +1 -1
  18. package/dist/core/config-loader.js +5 -2
  19. package/dist/core/config-loader.js.map +1 -1
  20. package/dist/core/server.d.ts +4 -0
  21. package/dist/core/server.d.ts.map +1 -1
  22. package/dist/core/server.js +240 -9
  23. package/dist/core/server.js.map +1 -1
  24. package/dist/core/vector.d.ts +4 -2
  25. package/dist/core/vector.d.ts.map +1 -1
  26. package/dist/core/vector.js +32 -2
  27. package/dist/core/vector.js.map +1 -1
  28. package/dist/errors/index.cjs +2 -0
  29. package/dist/index.cjs +1434 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +12 -1327
  33. package/dist/index.js.map +1 -1
  34. package/dist/index.mjs +153 -46
  35. package/dist/openapi/docs-ui.d.ts +1 -1
  36. package/dist/openapi/docs-ui.d.ts.map +1 -1
  37. package/dist/openapi/docs-ui.js +147 -35
  38. package/dist/openapi/docs-ui.js.map +1 -1
  39. package/dist/openapi/generator.d.ts.map +1 -1
  40. package/dist/openapi/generator.js +318 -6
  41. package/dist/openapi/generator.js.map +1 -1
  42. package/dist/start-vector.d.ts +3 -0
  43. package/dist/start-vector.d.ts.map +1 -0
  44. package/dist/start-vector.js +38 -0
  45. package/dist/start-vector.js.map +1 -0
  46. package/dist/types/index.d.ts +25 -0
  47. package/dist/types/index.d.ts.map +1 -1
  48. package/dist/utils/logger.js +1 -1
  49. package/dist/utils/validation.d.ts.map +1 -1
  50. package/dist/utils/validation.js +2 -0
  51. package/dist/utils/validation.js.map +1 -1
  52. package/package.json +10 -14
  53. package/src/auth/protected.ts +4 -0
  54. package/src/cache/manager.ts +4 -0
  55. package/src/cli/graceful-shutdown.ts +60 -0
  56. package/src/cli/index.ts +42 -49
  57. package/src/core/config-loader.ts +5 -2
  58. package/src/core/server.ts +304 -9
  59. package/src/core/vector.ts +38 -4
  60. package/src/index.ts +4 -3
  61. package/src/openapi/assets/favicon/android-chrome-192x192.png +0 -0
  62. package/src/openapi/assets/favicon/android-chrome-512x512.png +0 -0
  63. package/src/openapi/assets/favicon/apple-touch-icon.png +0 -0
  64. package/src/openapi/assets/favicon/favicon-16x16.png +0 -0
  65. package/src/openapi/assets/favicon/favicon-32x32.png +0 -0
  66. package/src/openapi/assets/favicon/favicon.ico +0 -0
  67. package/src/openapi/assets/favicon/site.webmanifest +11 -0
  68. package/src/openapi/assets/logo.svg +12 -0
  69. package/src/openapi/assets/logo_dark.svg +6 -0
  70. package/src/openapi/assets/logo_icon.png +0 -0
  71. package/src/openapi/assets/logo_white.svg +6 -0
  72. package/src/openapi/docs-ui.ts +153 -35
  73. package/src/openapi/generator.ts +341 -6
  74. package/src/start-vector.ts +50 -0
  75. package/src/types/index.ts +34 -0
  76. package/src/utils/logger.ts +1 -1
  77. package/src/utils/validation.ts +2 -0
@@ -1,11 +1,23 @@
1
1
  export function renderOpenAPIDocsHtml(
2
2
  spec: Record<string, unknown>,
3
3
  openapiPath: string,
4
- tailwindScriptPath: string
4
+ tailwindScriptPath: string,
5
+ logoDarkPath: string,
6
+ logoWhitePath: string,
7
+ appleTouchIconPath: string,
8
+ favicon32Path: string,
9
+ favicon16Path: string,
10
+ webManifestPath: string
5
11
  ): string {
6
12
  const specJson = JSON.stringify(spec).replace(/<\/script/gi, '<\\/script');
7
13
  const openapiPathJson = JSON.stringify(openapiPath);
8
14
  const tailwindScriptPathJson = JSON.stringify(tailwindScriptPath);
15
+ const logoDarkPathJson = JSON.stringify(logoDarkPath);
16
+ const logoWhitePathJson = JSON.stringify(logoWhitePath);
17
+ const appleTouchIconPathJson = JSON.stringify(appleTouchIconPath);
18
+ const favicon32PathJson = JSON.stringify(favicon32Path);
19
+ const favicon16PathJson = JSON.stringify(favicon16Path);
20
+ const webManifestPathJson = JSON.stringify(webManifestPath);
9
21
 
10
22
  return `<!DOCTYPE html>
11
23
  <html lang="en">
@@ -13,6 +25,10 @@ export function renderOpenAPIDocsHtml(
13
25
  <meta charset="UTF-8">
14
26
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
15
27
  <title>Vector API Documentation</title>
28
+ <link rel="apple-touch-icon" sizes="180x180" href=${appleTouchIconPathJson}>
29
+ <link rel="icon" type="image/png" sizes="32x32" href=${favicon32PathJson}>
30
+ <link rel="icon" type="image/png" sizes="16x16" href=${favicon16PathJson}>
31
+ <link rel="manifest" href=${webManifestPathJson}>
16
32
  <script>
17
33
  if (localStorage.getItem('theme') === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
18
34
  document.documentElement.classList.add('dark');
@@ -28,7 +44,12 @@ export function renderOpenAPIDocsHtml(
28
44
  theme: {
29
45
  extend: {
30
46
  colors: {
31
- brand: '#6366F1',
47
+ brand: {
48
+ DEFAULT: '#00A1FF',
49
+ mint: '#00FF8F',
50
+ soft: '#E4F5FF',
51
+ deep: '#007BC5',
52
+ },
32
53
  dark: { bg: '#0A0A0A', surface: '#111111', border: '#1F1F1F', text: '#EDEDED' },
33
54
  light: { bg: '#FFFFFF', surface: '#F9F9F9', border: '#E5E5E5', text: '#111111' }
34
55
  },
@@ -95,32 +116,44 @@ export function renderOpenAPIDocsHtml(
95
116
  from { opacity: 0; transform: translateX(-6px); }
96
117
  to { opacity: 1; transform: translateX(0); }
97
118
  }
119
+ @keyframes spin {
120
+ to { transform: rotate(360deg); }
121
+ }
122
+ .button-spinner {
123
+ display: inline-block;
124
+ width: 0.875rem;
125
+ height: 0.875rem;
126
+ border: 2px solid currentColor;
127
+ border-right-color: transparent;
128
+ border-radius: 9999px;
129
+ animation: spin 700ms linear infinite;
130
+ }
98
131
  @media (prefers-reduced-motion: reduce) {
99
132
  *, *::before, *::after {
100
133
  animation: none !important;
101
134
  transition: none !important;
102
135
  }
103
136
  }
104
- .json-key { color: #0f766e; }
105
- .json-string { color: #0369a1; }
106
- .json-number { color: #7c3aed; }
107
- .json-boolean { color: #b45309; }
108
- .json-null { color: #be123c; }
109
- .dark .json-key { color: #5eead4; }
110
- .dark .json-string { color: #7dd3fc; }
111
- .dark .json-number { color: #c4b5fd; }
112
- .dark .json-boolean { color: #fcd34d; }
113
- .dark .json-null { color: #fda4af; }
137
+ .json-key { color: #007bc5; }
138
+ .json-string { color: #334155; }
139
+ .json-number { color: #00a1ff; }
140
+ .json-boolean { color: #475569; }
141
+ .json-null { color: #64748b; }
142
+ .dark .json-key { color: #7dc9ff; }
143
+ .dark .json-string { color: #d1d9e6; }
144
+ .dark .json-number { color: #7dc9ff; }
145
+ .dark .json-boolean { color: #93a4bf; }
146
+ .dark .json-null { color: #7c8ba3; }
114
147
  </style>
115
148
  </head>
116
149
  <body class="bg-light-bg text-light-text dark:bg-dark-bg dark:text-dark-text font-sans antialiased flex h-screen overflow-hidden">
117
150
  <div id="mobile-backdrop" class="fixed inset-0 z-30 bg-black/40 opacity-0 pointer-events-none transition-opacity duration-300 md:hidden"></div>
118
151
  <aside id="docs-sidebar" class="fixed inset-y-0 left-0 z-40 w-72 md:w-64 border-r border-light-border dark:border-dark-border bg-light-surface dark:bg-dark-surface flex flex-col flex-shrink-0 transition-transform duration-300 ease-out -translate-x-full md:translate-x-0 md:static md:z-auto transition-colors duration-150">
119
152
  <div class="h-14 flex items-center px-5 border-b border-light-border dark:border-dark-border">
120
- <svg class="w-5 h-5 text-brand" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
121
- <polygon points="3 21 12 2 21 21 12 15 3 21"></polygon>
122
- </svg>
123
- <span class="ml-2.5 font-bold tracking-tight text-lg">Vector</span>
153
+ <div class="flex items-center">
154
+ <img src=${logoDarkPathJson} alt="Vector" class="h-6 w-auto block dark:hidden" />
155
+ <img src=${logoWhitePathJson} alt="Vector" class="h-6 w-auto hidden dark:block" />
156
+ </div>
124
157
  <button id="sidebar-close" class="ml-auto p-1.5 rounded-full border border-light-border dark:border-dark-border bg-light-bg/90 dark:bg-dark-bg/90 opacity-90 hover:opacity-100 transition md:hidden" aria-label="Close Menu" title="Close Menu">
125
158
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
126
159
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
@@ -151,8 +184,8 @@ export function renderOpenAPIDocsHtml(
151
184
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
152
185
  </svg>
153
186
  </button>
154
- <svg class="w-5 h-5 text-brand" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="3 21 12 2 21 21 12 15 3 21"></polygon></svg>
155
- <span class="font-bold tracking-tight">Vector</span>
187
+ <img src=${logoDarkPathJson} alt="Vector" class="h-5 w-auto block dark:hidden" />
188
+ <img src=${logoWhitePathJson} alt="Vector" class="h-5 w-auto hidden dark:block" />
156
189
  </div>
157
190
  <div class="flex-1"></div>
158
191
  <button id="theme-toggle" class="p-2 rounded-md hover:bg-black/5 dark:hover:bg-white/5 transition-colors" aria-label="Toggle Dark Mode">
@@ -177,17 +210,22 @@ export function renderOpenAPIDocsHtml(
177
210
  <div class="grid grid-cols-1 lg:grid-cols-12 gap-10">
178
211
  <div class="lg:col-span-5 space-y-8" id="params-column"></div>
179
212
  <div class="lg:col-span-7">
180
- <div class="rounded-lg border border-light-border dark:border-[#1F1F1F] bg-light-bg dark:bg-[#111111] overflow-hidden group">
181
- <div class="flex items-center justify-between px-4 py-2 border-b border-light-border dark:border-[#1F1F1F] bg-light-surface dark:bg-[#0A0A0A]">
182
- <span class="text-xs font-mono text-light-text/70 dark:text-[#EDEDED]/70">cURL</span>
183
- <button class="text-xs text-light-text/50 hover:text-light-text dark:text-[#EDEDED]/50 dark:hover:text-[#EDEDED] transition-colors" id="copy-curl">Copy</button>
213
+ <div class="rounded-lg border border-light-border dark:border-dark-border bg-light-bg dark:bg-dark-bg overflow-hidden group">
214
+ <div class="flex items-center justify-between px-4 py-2 border-b border-light-border dark:border-dark-border bg-light-surface dark:bg-dark-surface">
215
+ <span class="text-xs font-mono text-light-text/70 dark:text-dark-text/70">cURL</span>
216
+ <button class="text-xs text-light-text/50 hover:text-light-text dark:text-dark-text/50 dark:hover:text-dark-text transition-colors" id="copy-curl">Copy</button>
184
217
  </div>
185
- <pre class="p-4 text-sm font-mono text-light-text dark:text-[#EDEDED] overflow-x-auto leading-relaxed"><code id="curl-code"></code></pre>
218
+ <pre class="p-4 text-sm font-mono text-light-text dark:text-dark-text overflow-x-auto leading-relaxed"><code id="curl-code"></code></pre>
186
219
  </div>
187
220
  <div class="mt-4 p-4 rounded-lg border border-light-border dark:border-dark-border bg-light-surface dark:bg-dark-surface">
188
221
  <div class="flex items-center justify-between mb-3">
189
222
  <h4 class="text-sm font-medium">Try it out</h4>
190
- <button id="send-btn" class="px-4 py-1.5 bg-teal-600 text-white text-sm font-semibold rounded hover:bg-teal-500 transition-colors">Submit</button>
223
+ <button id="send-btn" class="px-4 py-1.5 bg-brand text-white text-sm font-semibold rounded hover:bg-brand-deep transition-colors">
224
+ <span class="inline-flex items-center gap-2">
225
+ <span id="send-btn-spinner" class="button-spinner hidden" aria-hidden="true"></span>
226
+ <span id="send-btn-label">Submit</span>
227
+ </span>
228
+ </button>
191
229
  </div>
192
230
  <div class="space-y-4">
193
231
  <div>
@@ -221,7 +259,7 @@ export function renderOpenAPIDocsHtml(
221
259
  </div>
222
260
  </div>
223
261
 
224
- <div>
262
+ <div id="response-section">
225
263
  <div class="flex items-center justify-between mb-2">
226
264
  <p class="text-xs font-semibold uppercase tracking-wider opacity-60">Response</p>
227
265
  </div>
@@ -248,7 +286,7 @@ export function renderOpenAPIDocsHtml(
248
286
  <div class="flex items-center justify-between mb-3">
249
287
  <h3 id="expand-modal-title" class="text-sm font-semibold">Expanded View</h3>
250
288
  <div class="flex items-center gap-2">
251
- <button id="expand-apply" class="hidden text-sm px-3 py-1.5 rounded bg-teal-600 text-white font-semibold hover:bg-teal-500 transition-colors">Apply</button>
289
+ <button id="expand-apply" class="hidden text-sm px-3 py-1.5 rounded bg-brand text-white font-semibold hover:bg-brand-deep transition-colors">Apply</button>
252
290
  <button id="expand-close" class="p-1.5 rounded-full border border-light-border dark:border-dark-border bg-light-bg/90 dark:bg-dark-bg/90 opacity-90 hover:opacity-100 hover:border-brand/60 transition-colors" aria-label="Close Modal" title="Close Modal">
253
291
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
254
292
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
@@ -267,12 +305,13 @@ export function renderOpenAPIDocsHtml(
267
305
  <script>
268
306
  const spec = ${specJson};
269
307
  const openapiPath = ${openapiPathJson};
308
+ const methodBadgeDefault = "bg-black/5 text-light-text/80 dark:bg-white/10 dark:text-dark-text/80";
270
309
  const methodBadge = {
271
- GET: "bg-green-100 text-green-700 dark:bg-green-500/10 dark:text-green-400",
272
- POST: "bg-blue-100 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400",
273
- PUT: "bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400",
274
- PATCH: "bg-amber-100 text-amber-700 dark:bg-amber-500/10 dark:text-amber-400",
275
- DELETE: "bg-red-100 text-red-700 dark:bg-red-500/10 dark:text-red-400",
310
+ GET: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
311
+ POST: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
312
+ PUT: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
313
+ PATCH: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
314
+ DELETE: "bg-brand-soft text-brand-deep dark:bg-brand/20 dark:text-brand",
276
315
  };
277
316
 
278
317
  function getOperations() {
@@ -526,6 +565,16 @@ export function renderOpenAPIDocsHtml(
526
565
  return;
527
566
  }
528
567
  for (const [tag, ops] of groups.entries()) {
568
+ ops.sort((a, b) => {
569
+ const byName = a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
570
+ if (byName !== 0) return byName;
571
+
572
+ const byPath = a.path.localeCompare(b.path, undefined, { sensitivity: "base" });
573
+ if (byPath !== 0) return byPath;
574
+
575
+ return a.method.localeCompare(b.method, undefined, { sensitivity: "base" });
576
+ });
577
+
529
578
  const block = document.createElement("div");
530
579
  block.innerHTML = '<h3 class="px-2 mb-2 font-semibold text-xs uppercase tracking-wider opacity-50"></h3><ul class="space-y-0.5"></ul>';
531
580
  block.querySelector("h3").textContent = tag;
@@ -537,14 +586,14 @@ export function renderOpenAPIDocsHtml(
537
586
  const a = document.createElement("a");
538
587
  a.href = "#";
539
588
  a.className = op === selected
540
- ? "block px-2 py-1.5 rounded-md bg-black/5 dark:bg-white/5 text-brand font-medium transition-colors"
589
+ ? "block px-2 py-1.5 rounded-md bg-brand-soft/70 dark:bg-brand/20 text-brand-deep dark:text-brand font-medium transition-colors"
541
590
  : "block px-2 py-1.5 rounded-md hover:bg-black/5 dark:hover:bg-white/5 transition-colors";
542
591
 
543
592
  const row = document.createElement("span");
544
593
  row.className = "flex items-center gap-2";
545
594
 
546
595
  const method = document.createElement("span");
547
- method.className = "px-1.5 py-0.5 rounded text-[10px] font-mono font-semibold " + (methodBadge[op.method] || "bg-zinc-100 text-zinc-700 dark:bg-zinc-500/10 dark:text-zinc-400");
596
+ method.className = "px-1.5 py-0.5 rounded text-[10px] font-mono font-semibold " + (methodBadge[op.method] || methodBadgeDefault);
548
597
  method.textContent = op.method;
549
598
 
550
599
  const name = document.createElement("span");
@@ -683,6 +732,55 @@ export function renderOpenAPIDocsHtml(
683
732
  );
684
733
  }
685
734
 
735
+ function renderResponseSchemasSection(responses) {
736
+ if (!responses || typeof responses !== "object") return "";
737
+
738
+ const statusCodes = Object.keys(responses).sort((a, b) => {
739
+ const aNum = Number(a);
740
+ const bNum = Number(b);
741
+ if (Number.isInteger(aNum) && Number.isInteger(bNum)) return aNum - bNum;
742
+ if (Number.isInteger(aNum)) return -1;
743
+ if (Number.isInteger(bNum)) return 1;
744
+ return a.localeCompare(b);
745
+ });
746
+
747
+ let sections = "";
748
+ for (const statusCode of statusCodes) {
749
+ const responseDef = responses[statusCode];
750
+ if (!responseDef || typeof responseDef !== "object") continue;
751
+
752
+ const jsonSchema =
753
+ responseDef.content &&
754
+ responseDef.content["application/json"] &&
755
+ responseDef.content["application/json"].schema;
756
+
757
+ if (!jsonSchema || typeof jsonSchema !== "object") continue;
758
+
759
+ const rootChildren = buildSchemaChildren(jsonSchema);
760
+ if (!rootChildren.length) continue;
761
+
762
+ let rows = "";
763
+ for (const child of rootChildren) {
764
+ rows += renderSchemaFieldNode(child, 0);
765
+ }
766
+
767
+ sections +=
768
+ '<div class="mb-4"><h4 class="text-xs font-mono uppercase tracking-wider opacity-70 mb-2">Status ' +
769
+ escapeHtml(statusCode) +
770
+ "</h4>" +
771
+ rows +
772
+ "</div>";
773
+ }
774
+
775
+ if (!sections) return "";
776
+
777
+ return (
778
+ '<div><h3 class="text-sm font-semibold mb-3 flex items-center border-b border-light-border dark:border-dark-border pb-2">Response Schemas</h3>' +
779
+ sections +
780
+ "</div>"
781
+ );
782
+ }
783
+
686
784
  function renderTryItParameterInputs(pathParams, queryParams) {
687
785
  const container = document.getElementById("request-param-inputs");
688
786
  if (!container || !selected) return;
@@ -966,6 +1064,19 @@ export function renderOpenAPIDocsHtml(
966
1064
  }
967
1065
  }
968
1066
 
1067
+ function setSubmitLoading(isLoading) {
1068
+ const sendButton = document.getElementById("send-btn");
1069
+ const spinner = document.getElementById("send-btn-spinner");
1070
+ const label = document.getElementById("send-btn-label");
1071
+ if (!sendButton) return;
1072
+
1073
+ sendButton.disabled = isLoading;
1074
+ sendButton.classList.toggle("opacity-80", isLoading);
1075
+ sendButton.classList.toggle("cursor-wait", isLoading);
1076
+ if (spinner) spinner.classList.toggle("hidden", !isLoading);
1077
+ if (label) label.textContent = isLoading ? "Sending..." : "Submit";
1078
+ }
1079
+
969
1080
  function updateRequestPreview() {
970
1081
  if (!selected) return;
971
1082
 
@@ -1029,7 +1140,7 @@ export function renderOpenAPIDocsHtml(
1029
1140
  document.getElementById("tag-description").textContent = op.description || "Interactive API documentation.";
1030
1141
  const methodNode = document.getElementById("endpoint-method");
1031
1142
  methodNode.textContent = selected.method;
1032
- methodNode.className = "px-2.5 py-0.5 rounded-full text-xs font-mono font-medium " + (methodBadge[selected.method] || "bg-zinc-100 text-zinc-700 dark:bg-zinc-500/10 dark:text-zinc-400");
1143
+ methodNode.className = "px-2.5 py-0.5 rounded-full text-xs font-mono font-medium " + (methodBadge[selected.method] || methodBadgeDefault);
1033
1144
  document.getElementById("endpoint-title").textContent = selected.name;
1034
1145
  document.getElementById("endpoint-path").textContent = selected.path;
1035
1146
 
@@ -1042,6 +1153,7 @@ export function renderOpenAPIDocsHtml(
1042
1153
  html += renderParamSection("Header Parameters", headers);
1043
1154
 
1044
1155
  html += renderRequestBodySchemaSection(reqSchema);
1156
+ html += renderResponseSchemasSection(op.responses);
1045
1157
  document.getElementById("params-column").innerHTML = html || '<div class="text-sm opacity-70">No parameters</div>';
1046
1158
  renderTryItParameterInputs(path, query);
1047
1159
  renderHeaderInputs();
@@ -1092,14 +1204,18 @@ export function renderOpenAPIDocsHtml(
1092
1204
  headers["Content-Type"] = "application/json";
1093
1205
  }
1094
1206
 
1207
+ setSubmitLoading(true);
1095
1208
  try {
1209
+ const requestStart = performance.now();
1096
1210
  const response = await fetch(requestPath, { method: selected.method, headers, body: body || undefined });
1097
1211
  const text = await response.text();
1212
+ const responseTimeMs = Math.round(performance.now() - requestStart);
1098
1213
  const contentType = response.headers.get("content-type") || "unknown";
1099
1214
  const formattedResponse = formatResponseText(text);
1100
1215
  const headerText =
1101
1216
  "Status: " + response.status + " " + response.statusText + "\\n" +
1102
- "Content-Type: " + contentType + "\\n\\n";
1217
+ "Content-Type: " + contentType + "\\n" +
1218
+ "Response Time: " + responseTimeMs + " ms\\n\\n";
1103
1219
  setResponseContent(
1104
1220
  headerText,
1105
1221
  formattedResponse.text,
@@ -1107,6 +1223,8 @@ export function renderOpenAPIDocsHtml(
1107
1223
  );
1108
1224
  } catch (error) {
1109
1225
  setResponseContent("", "Request failed: " + String(error), false);
1226
+ } finally {
1227
+ setSubmitLoading(false);
1110
1228
  }
1111
1229
  });
1112
1230