@vnphu/nestjs-api-explorer 0.1.0

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.
@@ -0,0 +1,1066 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getExplorerHtml = getExplorerHtml;
4
+ function getExplorerHtml(options) {
5
+ const { title, path } = options;
6
+ const config = JSON.stringify({ title, path });
7
+ return /* html */ `<!DOCTYPE html>
8
+ <html lang="en">
9
+ <head>
10
+ <meta charset="UTF-8" />
11
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
12
+ <title>${title}</title>
13
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
14
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
15
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
16
+ <style>
17
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
18
+
19
+ :root {
20
+ --bg: #f4f6fb;
21
+ --bg-white: #ffffff;
22
+ --bg-sidebar: #ffffff;
23
+ --bg-input: #f8fafc;
24
+ --bg-hover: #f1f5f9;
25
+ --bg-active: #eff6ff;
26
+ --border: #e2e8f0;
27
+ --border-2: #cbd5e1;
28
+ --text: #0f172a;
29
+ --text-muted: #64748b;
30
+ --text-dim: #94a3b8;
31
+ --accent: #3b82f6;
32
+ --accent-bg: #eff6ff;
33
+ --accent-bdr: #bfdbfe;
34
+
35
+ /* method colors */
36
+ --get-bg: #f0fdf4; --get-bdr: #bbf7d0; --get-fg: #15803d;
37
+ --post-bg: #eff6ff; --post-bdr:#bfdbfe; --post-fg: #1d4ed8;
38
+ --put-bg: #fff7ed; --put-bdr: #fed7aa; --put-fg: #c2410c;
39
+ --patch-bg:#fefce8; --patch-bdr:#fde68a;--patch-fg:#92400e;
40
+ --del-bg: #fef2f2; --del-bdr: #fecaca; --del-fg: #dc2626;
41
+ --head-bg: #faf5ff; --head-bdr:#e9d5ff; --head-fg: #7c3aed;
42
+ --opt-bg: #f8fafc; --opt-bdr: #e2e8f0; --opt-fg: #475569;
43
+
44
+ --radius: 8px;
45
+ --radius-sm: 5px;
46
+ --shadow-sm: 0 1px 3px rgba(0,0,0,.06), 0 1px 2px rgba(0,0,0,.04);
47
+ --shadow: 0 4px 12px rgba(0,0,0,.08), 0 1px 3px rgba(0,0,0,.05);
48
+ --font-ui: 'Inter', sans-serif;
49
+ --font-mono: 'JetBrains Mono', monospace;
50
+ }
51
+
52
+ html, body { height: 100%; overflow: hidden; background: var(--bg); color: var(--text); font-family: var(--font-ui); font-size: 13px; line-height: 1.5; }
53
+
54
+ ::-webkit-scrollbar { width: 5px; height: 5px; }
55
+ ::-webkit-scrollbar-track { background: transparent; }
56
+ ::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 3px; }
57
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-dim); }
58
+
59
+ /* ── Layout ── */
60
+ #app { display: flex; flex-direction: column; height: 100vh; }
61
+
62
+ /* ── Header ── */
63
+ #header {
64
+ display: flex; align-items: center; gap: 12px;
65
+ padding: 0 18px; height: 54px; flex-shrink: 0;
66
+ background: var(--bg-white);
67
+ border-bottom: 1px solid var(--border);
68
+ box-shadow: var(--shadow-sm);
69
+ z-index: 10;
70
+ }
71
+ .logo { display: flex; align-items: center; gap: 9px; text-decoration: none; }
72
+ .logo-icon {
73
+ width: 30px; height: 30px; border-radius: 8px;
74
+ background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
75
+ display: flex; align-items: center; justify-content: center;
76
+ box-shadow: 0 2px 8px rgba(59,130,246,.35);
77
+ }
78
+ .logo-icon svg { color: #fff; }
79
+ .logo-text { font-weight: 700; font-size: 15px; color: var(--text); letter-spacing: -0.4px; }
80
+ .logo-text span { color: var(--accent); }
81
+
82
+ .env-tag {
83
+ font-size: 10px; font-weight: 600; letter-spacing: 0.6px; text-transform: uppercase;
84
+ padding: 2px 8px; border-radius: 20px;
85
+ background: var(--bg-hover); border: 1px solid var(--border); color: var(--text-muted);
86
+ }
87
+ .env-tag.dev { background: #f0fdf4; border-color: #bbf7d0; color: #15803d; }
88
+
89
+ .header-sep { width: 1px; height: 22px; background: var(--border); margin: 0 4px; }
90
+ .header-spacer { flex: 1; }
91
+
92
+ #base-url-group { display: flex; align-items: center; gap: 0; flex: 1; max-width: 380px; }
93
+ .base-url-label {
94
+ font-size: 11px; font-weight: 600; color: var(--text-muted); padding: 0 10px;
95
+ white-space: nowrap; letter-spacing: 0.3px;
96
+ }
97
+ #base-url {
98
+ flex: 1; height: 34px; padding: 0 11px;
99
+ background: var(--bg-input); border: 1.5px solid var(--border);
100
+ border-radius: var(--radius); color: var(--text); font-family: var(--font-mono);
101
+ font-size: 12px; outline: none; transition: border-color .15s;
102
+ }
103
+ #base-url:focus { border-color: var(--accent); background: var(--bg-white); box-shadow: 0 0 0 3px rgba(59,130,246,.1); }
104
+
105
+ .icon-btn {
106
+ width: 34px; height: 34px; display: flex; align-items: center; justify-content: center;
107
+ border-radius: var(--radius); border: 1.5px solid var(--border);
108
+ background: var(--bg-white); color: var(--text-muted);
109
+ cursor: pointer; transition: all .15s; flex-shrink: 0;
110
+ }
111
+ .icon-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-bg); }
112
+
113
+ /* ── Main ── */
114
+ #main { display: flex; flex: 1; overflow: hidden; }
115
+
116
+ /* ── Sidebar ── */
117
+ #sidebar {
118
+ width: 268px; flex-shrink: 0; display: flex; flex-direction: column;
119
+ background: var(--bg-sidebar); border-right: 1px solid var(--border);
120
+ overflow: hidden;
121
+ }
122
+ .sidebar-header {
123
+ padding: 12px 14px 8px; border-bottom: 1px solid var(--border);
124
+ }
125
+ .sidebar-title {
126
+ font-size: 11px; font-weight: 600; color: var(--text-muted);
127
+ text-transform: uppercase; letter-spacing: 0.7px; margin-bottom: 8px;
128
+ }
129
+ #search-wrap { position: relative; }
130
+ #search {
131
+ width: 100%; height: 32px; padding: 0 10px 0 32px;
132
+ background: var(--bg-input); border: 1.5px solid var(--border);
133
+ border-radius: var(--radius-sm); color: var(--text); font-family: var(--font-ui);
134
+ font-size: 12px; outline: none; transition: all .15s;
135
+ }
136
+ #search:focus { border-color: var(--accent); background: var(--bg-white); box-shadow: 0 0 0 3px rgba(59,130,246,.1); }
137
+ #search::placeholder { color: var(--text-dim); }
138
+ .search-icon { position: absolute; left: 10px; top: 50%; transform: translateY(-50%); color: var(--text-dim); pointer-events: none; }
139
+
140
+ #route-list { flex: 1; overflow-y: auto; padding: 6px 8px 16px; min-height: 0; }
141
+ .route-count { font-size: 10px; color: var(--text-dim); padding: 8px 6px 4px; font-weight: 500; }
142
+
143
+ .route-item {
144
+ display: flex; align-items: center; gap: 8px;
145
+ padding: 7px 9px; border-radius: var(--radius-sm);
146
+ cursor: pointer; transition: background .12s; border: 1.5px solid transparent;
147
+ margin-bottom: 1px;
148
+ }
149
+ .route-item:hover { background: var(--bg-hover); }
150
+ .route-item.active {
151
+ background: var(--accent-bg); border-color: var(--accent-bdr);
152
+ }
153
+ .route-item.active .route-path { color: var(--text); font-weight: 500; }
154
+
155
+ .method-badge {
156
+ font-family: var(--font-mono); font-size: 9px; font-weight: 700;
157
+ padding: 2px 6px; border-radius: 4px; letter-spacing: 0.2px;
158
+ min-width: 46px; text-align: center; text-transform: uppercase;
159
+ flex-shrink: 0; border-width: 1px; border-style: solid;
160
+ }
161
+ .method-GET { background: var(--get-bg); border-color: var(--get-bdr); color: var(--get-fg); }
162
+ .method-POST { background: var(--post-bg); border-color: var(--post-bdr); color: var(--post-fg); }
163
+ .method-PUT { background: var(--put-bg); border-color: var(--put-bdr); color: var(--put-fg); }
164
+ .method-PATCH { background: var(--patch-bg); border-color: var(--patch-bdr); color: var(--patch-fg); }
165
+ .method-DELETE { background: var(--del-bg); border-color: var(--del-bdr); color: var(--del-fg); }
166
+ .method-HEAD { background: var(--head-bg); border-color: var(--head-bdr); color: var(--head-fg); }
167
+ .method-OPTIONS{ background: var(--opt-bg); border-color: var(--opt-bdr); color: var(--opt-fg); }
168
+
169
+ .route-path {
170
+ font-family: var(--font-mono); font-size: 11.5px; color: var(--text-muted);
171
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;
172
+ }
173
+ .empty-routes { padding: 32px 16px; text-align: center; color: var(--text-dim); font-size: 12px; line-height: 1.7; }
174
+
175
+ /* ── Sidebar resize ── */
176
+ #sidebar-resize { width: 4px; cursor: col-resize; background: transparent; flex-shrink: 0; transition: background .15s; }
177
+ #sidebar-resize:hover, #sidebar-resize.dragging { background: var(--accent); opacity: .4; }
178
+
179
+ /* ── Content ── */
180
+ #content { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
181
+
182
+ /* ── URL bar ── */
183
+ #url-bar {
184
+ display: flex; align-items: center; gap: 8px;
185
+ padding: 10px 16px; border-bottom: 1px solid var(--border);
186
+ background: var(--bg-white); flex-shrink: 0;
187
+ }
188
+ #method-pill {
189
+ font-family: var(--font-mono); font-size: 11px; font-weight: 700;
190
+ padding: 5px 11px; border-radius: var(--radius-sm); flex-shrink: 0;
191
+ border-width: 1px; border-style: solid;
192
+ }
193
+ #url-display {
194
+ flex: 1; height: 38px; padding: 0 12px;
195
+ background: var(--bg-input); border: 1.5px solid var(--border);
196
+ border-radius: var(--radius); color: var(--text); font-family: var(--font-mono);
197
+ font-size: 12px; outline: none; transition: all .15s;
198
+ }
199
+ #url-display:focus { border-color: var(--accent); background: var(--bg-white); }
200
+
201
+ #send-btn {
202
+ height: 38px; padding: 0 22px; border-radius: var(--radius);
203
+ background: var(--accent); border: none; color: #fff;
204
+ font-family: var(--font-ui); font-weight: 600; font-size: 13px;
205
+ cursor: pointer; transition: all .15s; flex-shrink: 0; letter-spacing: 0.2px;
206
+ box-shadow: 0 1px 4px rgba(59,130,246,.3);
207
+ }
208
+ #send-btn:hover { background: #2563eb; box-shadow: 0 2px 8px rgba(59,130,246,.4); }
209
+ #send-btn:active { background: #1d4ed8; }
210
+ #send-btn:disabled { background: var(--bg-hover); color: var(--text-dim); box-shadow: none; cursor: not-allowed; }
211
+ .spinner { display: inline-block; width: 14px; height: 14px; border: 2px solid rgba(255,255,255,.3); border-top-color: #fff; border-radius: 50%; animation: spin .6s linear infinite; vertical-align: middle; }
212
+ @keyframes spin { to { transform: rotate(360deg); } }
213
+
214
+ /* ── Panels ── */
215
+ #panels { flex: 1; display: flex; flex-direction: column; overflow: hidden; background: var(--bg); padding: 8px; gap: 0; }
216
+
217
+ /* ── Tabs ── */
218
+ .tab-bar {
219
+ display: flex; align-items: center; gap: 0; padding: 0 16px;
220
+ background: var(--bg-white); border-bottom: 1px solid var(--border);
221
+ flex-shrink: 0; height: 40px;
222
+ }
223
+ .tab-btn {
224
+ display: flex; align-items: center; gap: 5px;
225
+ padding: 0 14px; height: 40px;
226
+ font-family: var(--font-ui); font-size: 12.5px; font-weight: 500;
227
+ color: var(--text-muted); background: transparent; border: none;
228
+ border-bottom: 2px solid transparent; cursor: pointer; transition: all .15s;
229
+ white-space: nowrap; margin-bottom: -1px;
230
+ }
231
+ .tab-btn:hover { color: var(--text); }
232
+ .tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; }
233
+
234
+ .tab-badge {
235
+ display: inline-flex; align-items: center; justify-content: center;
236
+ min-width: 17px; height: 17px; padding: 0 4px;
237
+ font-size: 10px; font-weight: 700; border-radius: 9px;
238
+ background: var(--accent-bg); color: var(--accent); border: 1px solid var(--accent-bdr);
239
+ }
240
+
241
+ .tab-panel { display: none; flex: 1; min-height: 0; }
242
+ .tab-panel.active { display: flex; flex-direction: column; gap: 6px; flex: 1; overflow-y: auto; padding: 14px 16px; min-height: 0; }
243
+
244
+ /* ── Request Panel ── */
245
+ #req-panel {
246
+ display: flex; flex-direction: column; overflow: hidden;
247
+ background: var(--bg-white);
248
+ border-radius: var(--radius);
249
+ border: 1px solid var(--border); box-shadow: var(--shadow-sm);
250
+ flex: 0 0 300px; /* default height, user can drag to resize */
251
+ }
252
+ #req-panel-inner { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-height: 0; }
253
+
254
+ .section-label {
255
+ font-size: 10.5px; font-weight: 600; color: var(--text-muted);
256
+ text-transform: uppercase; letter-spacing: 0.6px; padding-bottom: 8px;
257
+ }
258
+ .divider { height: 1px; background: var(--border); margin: 10px 0; }
259
+
260
+ /* ── KV rows ── */
261
+ .kv-row { display: grid; grid-template-columns: 20px 1fr 1fr 26px; gap: 5px; align-items: center; }
262
+ .kv-checkbox { width: 14px; height: 14px; accent-color: var(--accent); cursor: pointer; flex-shrink: 0; }
263
+ .kv-input {
264
+ height: 30px; padding: 0 9px;
265
+ background: var(--bg-input); border: 1.5px solid var(--border);
266
+ border-radius: var(--radius-sm); color: var(--text); font-family: var(--font-mono);
267
+ font-size: 11.5px; outline: none; transition: all .15s;
268
+ }
269
+ .kv-input:focus { border-color: var(--accent); background: var(--bg-white); }
270
+ .kv-input::placeholder { color: var(--text-dim); }
271
+ .kv-remove {
272
+ width: 22px; height: 22px; display: flex; align-items: center; justify-content: center;
273
+ border-radius: 4px; border: none; background: transparent;
274
+ color: var(--text-dim); cursor: pointer; font-size: 16px; line-height: 1;
275
+ transition: all .12s;
276
+ }
277
+ .kv-remove:hover { background: #fee2e2; color: #dc2626; }
278
+ .add-row-btn {
279
+ display: flex; align-items: center; gap: 6px; padding: 5px 2px;
280
+ font-size: 12px; font-weight: 500; color: var(--accent);
281
+ background: none; border: none; cursor: pointer; transition: opacity .12s; margin-top: 4px;
282
+ }
283
+ .add-row-btn:hover { opacity: .75; }
284
+
285
+ /* Path params */
286
+ .param-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; align-items: center; }
287
+ .param-name {
288
+ font-family: var(--font-mono); font-size: 11.5px; color: var(--put-fg);
289
+ padding: 5px 10px; background: var(--put-bg); border-radius: var(--radius-sm);
290
+ border: 1px solid var(--put-bdr); font-weight: 500;
291
+ }
292
+
293
+ /* Auth */
294
+ .auth-type-row { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 14px; }
295
+ .auth-type-btn {
296
+ padding: 5px 13px; border-radius: 20px;
297
+ border: 1.5px solid var(--border);
298
+ background: var(--bg-input); color: var(--text-muted);
299
+ font-family: var(--font-ui); font-size: 12px; font-weight: 500;
300
+ cursor: pointer; transition: all .12s;
301
+ }
302
+ .auth-type-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-bg); }
303
+ .auth-type-btn.active { border-color: var(--accent); color: var(--accent); background: var(--accent-bg); font-weight: 600; }
304
+
305
+ /* Body */
306
+ .body-type-row { display: flex; gap: 6px; margin-bottom: 10px; }
307
+ .body-type-btn {
308
+ padding: 4px 12px; border-radius: 20px;
309
+ border: 1.5px solid var(--border); background: var(--bg-input);
310
+ color: var(--text-muted); font-family: var(--font-mono); font-size: 11px;
311
+ cursor: pointer; transition: all .12s;
312
+ }
313
+ .body-type-btn:hover { border-color: var(--accent); color: var(--accent); }
314
+ .body-type-btn.active { border-color: var(--accent); background: var(--accent-bg); color: var(--accent); font-weight: 600; }
315
+
316
+ #body-editor {
317
+ flex: 1; min-height: 100px; padding: 10px 12px; resize: none;
318
+ background: var(--bg-input); border: 1.5px solid var(--border);
319
+ border-radius: var(--radius); color: var(--text); font-family: var(--font-mono);
320
+ font-size: 12px; line-height: 1.65; outline: none; tab-size: 2; transition: all .15s;
321
+ }
322
+ #body-editor:focus { border-color: var(--accent); background: var(--bg-white); }
323
+
324
+ #format-btn {
325
+ align-self: flex-start; padding: 4px 11px; border-radius: var(--radius-sm);
326
+ border: 1.5px solid var(--border); background: var(--bg-white);
327
+ color: var(--text-muted); font-family: var(--font-ui); font-size: 11px; font-weight: 500;
328
+ cursor: pointer; transition: all .12s; margin-top: 6px;
329
+ }
330
+ #format-btn:hover { border-color: var(--accent); color: var(--accent); }
331
+
332
+ /* Select */
333
+ .styled-select {
334
+ height: 30px; padding: 0 9px; background: var(--bg-input); border: 1.5px solid var(--border);
335
+ border-radius: var(--radius-sm); color: var(--text); font-family: var(--font-ui);
336
+ font-size: 12px; outline: none; cursor: pointer; transition: all .15s;
337
+ }
338
+ .styled-select:focus { border-color: var(--accent); }
339
+
340
+ /* ── Resize Handle ── */
341
+ #resize-handle {
342
+ height: 8px; cursor: row-resize; flex-shrink: 0;
343
+ display: flex; align-items: center; justify-content: center;
344
+ background: transparent; transition: background .15s;
345
+ }
346
+ #resize-handle::after {
347
+ content: ''; display: block; width: 40px; height: 3px;
348
+ border-radius: 2px; background: var(--border); transition: background .15s;
349
+ }
350
+ #resize-handle:hover::after, #resize-handle.dragging::after { background: var(--accent); }
351
+
352
+ /* ── Response Panel ── */
353
+ #res-panel {
354
+ display: flex; flex-direction: column; overflow: hidden;
355
+ background: var(--bg-white);
356
+ border-radius: var(--radius);
357
+ border: 1px solid var(--border); box-shadow: var(--shadow-sm);
358
+ flex: 1; min-height: 140px;
359
+ }
360
+
361
+ #res-header {
362
+ display: flex; align-items: center; gap: 10px;
363
+ padding: 8px 16px; border-bottom: 1px solid var(--border); flex-shrink: 0; min-height: 42px;
364
+ background: var(--bg-white);
365
+ }
366
+ .res-label { font-size: 11px; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; }
367
+
368
+ .status-pill {
369
+ font-family: var(--font-mono); font-size: 11px; font-weight: 700;
370
+ padding: 3px 10px; border-radius: 20px; border-width: 1px; border-style: solid;
371
+ }
372
+ .status-2xx { background: var(--get-bg); border-color: var(--get-bdr); color: var(--get-fg); }
373
+ .status-3xx { background: #eff6ff; border-color: #bfdbfe; color: #1d4ed8; }
374
+ .status-4xx { background: var(--put-bg); border-color: var(--put-bdr); color: var(--put-fg); }
375
+ .status-5xx { background: var(--del-bg); border-color: var(--del-bdr); color: var(--del-fg); }
376
+ .status-err { background: var(--del-bg); border-color: var(--del-bdr); color: var(--del-fg); }
377
+
378
+ .res-meta { font-family: var(--font-mono); font-size: 11px; color: var(--text-muted); }
379
+ .res-spacer { flex: 1; }
380
+
381
+ #copy-btn {
382
+ display: flex; align-items: center; gap: 5px; padding: 4px 11px;
383
+ border-radius: var(--radius-sm); border: 1.5px solid var(--border);
384
+ background: var(--bg-white); color: var(--text-muted);
385
+ font-family: var(--font-ui); font-size: 11px; font-weight: 500;
386
+ cursor: pointer; transition: all .12s;
387
+ }
388
+ #copy-btn:hover { border-color: var(--accent); color: var(--accent); background: var(--accent-bg); }
389
+
390
+ #res-body { flex: 1; overflow-y: auto; padding: 14px 16px; }
391
+ #res-empty {
392
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
393
+ flex: 1; min-height: 100px; gap: 10px; color: var(--text-dim); padding: 20px;
394
+ }
395
+ #res-empty p { font-size: 12.5px; text-align: center; line-height: 1.6; color: var(--text-muted); }
396
+ #res-empty kbd {
397
+ display: inline-block; padding: 1px 6px; border-radius: 4px;
398
+ background: var(--bg-hover); border: 1px solid var(--border);
399
+ font-family: var(--font-mono); font-size: 11px; color: var(--text-muted);
400
+ }
401
+
402
+ #res-headers-list { display: flex; flex-direction: column; gap: 2px; padding: 12px 16px; overflow-y: auto; flex: 1; }
403
+ .res-header-row {
404
+ display: grid; grid-template-columns: 200px 1fr; gap: 12px;
405
+ padding: 5px 0; border-bottom: 1px solid var(--border); font-size: 12px;
406
+ }
407
+ .res-header-row:last-child { border-bottom: none; }
408
+ .res-header-key { font-family: var(--font-mono); font-weight: 600; color: var(--accent); }
409
+ .res-header-val { font-family: var(--font-mono); color: var(--text-muted); word-break: break-all; }
410
+
411
+ /* ── JSON highlight ── */
412
+ pre.json-body {
413
+ font-family: var(--font-mono); font-size: 12.5px; line-height: 1.7;
414
+ white-space: pre-wrap; word-break: break-all; color: var(--text);
415
+ }
416
+ .json-key { color: #0f766e; }
417
+ .json-string { color: #16a34a; }
418
+ .json-number { color: #d97706; }
419
+ .json-boolean { color: #7c3aed; }
420
+ .json-null { color: #94a3b8; font-style: italic; }
421
+ .raw-body { font-family: var(--font-mono); font-size: 12px; line-height: 1.7; color: var(--text-muted); white-space: pre-wrap; word-break: break-all; }
422
+
423
+ /* ── No-route state ── */
424
+ #no-route-state {
425
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
426
+ flex: 1; gap: 12px; color: var(--text-dim); padding: 40px;
427
+ }
428
+ #no-route-state .icon-wrap {
429
+ width: 64px; height: 64px; border-radius: 16px;
430
+ background: var(--bg-hover); border: 1px solid var(--border);
431
+ display: flex; align-items: center; justify-content: center;
432
+ }
433
+ #no-route-state h3 { font-size: 15px; font-weight: 600; color: var(--text-muted); }
434
+ #no-route-state p { font-size: 12.5px; color: var(--text-dim); max-width: 260px; text-align: center; line-height: 1.7; }
435
+
436
+ /* ── Helpers ── */
437
+ .flex-col { display: flex; flex-direction: column; }
438
+ .gap-6 { gap: 6px; }
439
+ .hidden { display: none !important; }
440
+ .text-muted { color: var(--text-muted); font-size: 12px; line-height: 1.6; }
441
+ .field-label { font-size: 11px; font-weight: 600; color: var(--text-muted); margin-bottom: 5px; display: block; }
442
+ .field-group { display: flex; flex-direction: column; }
443
+ </style>
444
+ </head>
445
+ <body>
446
+ <div id="app">
447
+
448
+ <!-- Header -->
449
+ <header id="header">
450
+ <div class="logo">
451
+ <div class="logo-icon">
452
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
453
+ <polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/>
454
+ </svg>
455
+ </div>
456
+ <span class="logo-text">API<span>Explorer</span></span>
457
+ </div>
458
+ <span class="env-tag" id="env-tag">ENV</span>
459
+ <div class="header-sep"></div>
460
+ <div class="header-spacer"></div>
461
+ <div id="base-url-group">
462
+ <span class="base-url-label">Base URL</span>
463
+ <input id="base-url" type="url" spellcheck="false" />
464
+ </div>
465
+ <button class="icon-btn" id="reload-btn" title="Reload routes (r)">
466
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
467
+ <polyline points="23 4 23 10 17 10"/>
468
+ <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
469
+ </svg>
470
+ </button>
471
+ </header>
472
+
473
+ <!-- Main -->
474
+ <div id="main">
475
+
476
+ <!-- Sidebar -->
477
+ <aside id="sidebar">
478
+ <div class="sidebar-header">
479
+ <div class="sidebar-title">Routes</div>
480
+ <div id="search-wrap">
481
+ <svg class="search-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
482
+ <circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
483
+ </svg>
484
+ <input id="search" type="text" placeholder="Search routes…" autocomplete="off" spellcheck="false" />
485
+ </div>
486
+ </div>
487
+ <div id="route-list"></div>
488
+ </aside>
489
+
490
+ <div id="sidebar-resize"></div>
491
+
492
+ <!-- Content -->
493
+ <div id="content">
494
+
495
+ <!-- URL bar -->
496
+ <div id="url-bar">
497
+ <span id="method-pill" class="method-badge method-GET" style="visibility:hidden">GET</span>
498
+ <input id="url-display" type="text" spellcheck="false" placeholder="Select a route from the sidebar…" readonly />
499
+ <button id="send-btn" disabled>Send</button>
500
+ </div>
501
+
502
+ <!-- Panels -->
503
+ <div id="panels">
504
+
505
+ <!-- Request Panel -->
506
+ <div id="req-panel">
507
+ <div class="tab-bar" id="req-tab-bar">
508
+ <button class="tab-btn active" data-tab="params">Params</button>
509
+ <button class="tab-btn" data-tab="headers">Headers</button>
510
+ <button class="tab-btn" data-tab="auth">Auth</button>
511
+ <button class="tab-btn" data-tab="body">Body</button>
512
+ </div>
513
+ <div id="req-panel-inner">
514
+
515
+ <!-- Params -->
516
+ <div class="tab-panel active" id="tab-params">
517
+ <div id="path-params-section" class="hidden">
518
+ <div class="section-label">Path Parameters</div>
519
+ <div id="path-params-rows" class="flex-col gap-6"></div>
520
+ <div class="divider"></div>
521
+ </div>
522
+ <div class="section-label">Query Parameters</div>
523
+ <div id="query-params-rows" class="flex-col gap-6"></div>
524
+ <button class="add-row-btn" id="add-query-btn">
525
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
526
+ Add parameter
527
+ </button>
528
+ </div>
529
+
530
+ <!-- Headers -->
531
+ <div class="tab-panel" id="tab-headers">
532
+ <div id="req-headers-rows" class="flex-col gap-6"></div>
533
+ <button class="add-row-btn" id="add-header-btn">
534
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
535
+ Add header
536
+ </button>
537
+ </div>
538
+
539
+ <!-- Auth -->
540
+ <div class="tab-panel" id="tab-auth">
541
+ <div class="auth-type-row">
542
+ <button class="auth-type-btn active" data-auth="none">None</button>
543
+ <button class="auth-type-btn" data-auth="bearer">Bearer Token</button>
544
+ <button class="auth-type-btn" data-auth="basic">Basic Auth</button>
545
+ <button class="auth-type-btn" data-auth="apikey">API Key</button>
546
+ </div>
547
+ <div id="auth-none-panel" class="text-muted">No authentication will be sent with this request.</div>
548
+ <div id="auth-bearer-panel" class="hidden flex-col gap-6">
549
+ <div class="field-group">
550
+ <label class="field-label">Token</label>
551
+ <input class="kv-input" id="bearer-token" type="text" style="width:100%" placeholder="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…" autocomplete="off" spellcheck="false" />
552
+ </div>
553
+ </div>
554
+ <div id="auth-basic-panel" class="hidden flex-col gap-6">
555
+ <div class="field-group">
556
+ <label class="field-label">Username</label>
557
+ <input class="kv-input" id="basic-username" type="text" style="width:100%" placeholder="username" autocomplete="off" spellcheck="false" />
558
+ </div>
559
+ <div class="field-group">
560
+ <label class="field-label">Password</label>
561
+ <input class="kv-input" id="basic-password" type="password" style="width:100%" placeholder="••••••••" autocomplete="off" />
562
+ </div>
563
+ </div>
564
+ <div id="auth-apikey-panel" class="hidden flex-col gap-6">
565
+ <div class="field-group">
566
+ <label class="field-label">Key Name</label>
567
+ <input class="kv-input" id="apikey-name" type="text" style="width:100%" placeholder="X-API-Key" value="X-API-Key" spellcheck="false" />
568
+ </div>
569
+ <div class="field-group">
570
+ <label class="field-label">Key Value</label>
571
+ <input class="kv-input" id="apikey-value" type="text" style="width:100%" placeholder="your-secret-key" autocomplete="off" spellcheck="false" />
572
+ </div>
573
+ <div class="field-group">
574
+ <label class="field-label">Add To</label>
575
+ <select class="styled-select" id="apikey-in" style="width:160px">
576
+ <option value="header">Header</option>
577
+ <option value="query">Query String</option>
578
+ </select>
579
+ </div>
580
+ </div>
581
+ </div>
582
+
583
+ <!-- Body -->
584
+ <div class="tab-panel" id="tab-body">
585
+ <div class="body-type-row">
586
+ <button class="body-type-btn active" data-body="none">None</button>
587
+ <button class="body-type-btn" data-body="json">JSON</button>
588
+ <button class="body-type-btn" data-body="form">Form</button>
589
+ <button class="body-type-btn" data-body="text">Text</button>
590
+ </div>
591
+ <div id="body-editor-wrap" class="hidden flex-col" style="flex:1">
592
+ <textarea id="body-editor" placeholder='{\n "key": "value"\n}'></textarea>
593
+ <button id="format-btn">⌥ Format JSON</button>
594
+ </div>
595
+ <div id="body-none-msg" class="text-muted">No request body will be sent with this request.</div>
596
+ </div>
597
+
598
+ </div>
599
+ </div>
600
+
601
+ <!-- Resize handle -->
602
+ <div id="resize-handle"></div>
603
+
604
+ <!-- Response Panel -->
605
+ <div id="res-panel">
606
+ <div id="res-header">
607
+ <span class="res-label">Response</span>
608
+ <span id="status-pill"></span>
609
+ <span class="res-meta" id="res-time"></span>
610
+ <span class="res-meta" id="res-size"></span>
611
+ <div class="res-spacer"></div>
612
+ <button id="copy-btn" class="hidden">
613
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
614
+ <rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
615
+ </svg>
616
+ Copy
617
+ </button>
618
+ </div>
619
+ <div class="tab-bar" id="res-tab-bar">
620
+ <button class="tab-btn active" data-tab="body">Body</button>
621
+ <button class="tab-btn" data-tab="headers">Headers <span class="tab-badge" id="res-headers-count" style="display:none"></span></button>
622
+ </div>
623
+ <div id="res-body-panel" style="flex:1;overflow:hidden;display:flex;flex-direction:column">
624
+ <div id="res-empty">
625
+ <div class="icon-wrap" style="width:52px;height:52px;border-radius:14px;background:var(--bg-hover);border:1px solid var(--border);display:flex;align-items:center;justify-content:center">
626
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--text-dim)" stroke-width="1.5"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
627
+ </div>
628
+ <p>Select a route and click <strong>Send</strong><br/>or press <kbd>⌘ Enter</kbd> to make a request.</p>
629
+ </div>
630
+ <div id="res-body" class="hidden"></div>
631
+ </div>
632
+ <div id="res-headers-panel" class="hidden" style="flex:1;overflow:hidden;display:flex;flex-direction:column">
633
+ <div id="res-headers-list"></div>
634
+ </div>
635
+ </div>
636
+
637
+ </div>
638
+ </div>
639
+ </div>
640
+ </div>
641
+
642
+ <script>
643
+ const CONFIG = ${config};
644
+
645
+ // ── State ──────────────────────────────────────────────────────────
646
+ const S = {
647
+ routes: [], filtered: [], selected: null,
648
+ pathParams: {}, queryParams: [], reqHeaders: [],
649
+ auth: { type: 'none', bearerToken: '', basicUsername: '', basicPassword: '', apiKeyName: 'X-API-Key', apiKeyValue: '', apiKeyIn: 'header' },
650
+ body: { type: 'none', content: '' },
651
+ response: null, reqTab: 'params', resTab: 'body', loading: false, _uid: 0,
652
+ };
653
+ function uid() { return ++S._uid; }
654
+
655
+ // ── Elements ───────────────────────────────────────────────────────
656
+ const $ = id => document.getElementById(id);
657
+ const el = {
658
+ envTag: $('env-tag'), baseUrl: $('base-url'), reloadBtn: $('reload-btn'),
659
+ search: $('search'), routeList: $('route-list'),
660
+ methodPill: $('method-pill'), urlDisplay: $('url-display'), sendBtn: $('send-btn'),
661
+ reqTabBar: $('req-tab-bar'),
662
+ pathParamsSec: $('path-params-section'), pathParamsRows: $('path-params-rows'),
663
+ queryParamsRows: $('query-params-rows'), addQueryBtn: $('add-query-btn'),
664
+ reqHeadersRows: $('req-headers-rows'), addHeaderBtn: $('add-header-btn'),
665
+ bearerToken: $('bearer-token'), basicUsername: $('basic-username'), basicPassword: $('basic-password'),
666
+ apikeyName: $('apikey-name'), apikeyValue: $('apikey-value'), apikeyIn: $('apikey-in'),
667
+ bodyEditor: $('body-editor'), formatBtn: $('format-btn'),
668
+ bodyEditorWrap: $('body-editor-wrap'), bodyNoneMsg: $('body-none-msg'),
669
+ resizeHandle: $('resize-handle'), sidebarResize: $('sidebar-resize'),
670
+ statusPill: $('status-pill'), resTime: $('res-time'), resSize: $('res-size'),
671
+ copyBtn: $('copy-btn'), resHeadersCount: $('res-headers-count'),
672
+ resTabBar: $('res-tab-bar'), resEmpty: $('res-empty'), resBody: $('res-body'),
673
+ resBodyPanel: $('res-body-panel'), resHeadersPanel: $('res-headers-panel'),
674
+ resHeadersList: $('res-headers-list'),
675
+ reqPanel: $('req-panel'), resPanel: $('res-panel'), panels: $('panels'),
676
+ sidebar: $('sidebar'),
677
+ };
678
+
679
+ // ── Init ───────────────────────────────────────────────────────────
680
+ function init() {
681
+ el.baseUrl.value = window.location.origin;
682
+ const host = window.location.hostname;
683
+ const isLocal = host === 'localhost' || host === '127.0.0.1' || host.endsWith('.local');
684
+ el.envTag.textContent = isLocal ? 'dev' : 'staging';
685
+ if (isLocal) el.envTag.classList.add('dev');
686
+ bindEvents();
687
+ loadRoutes();
688
+ }
689
+
690
+ // ── Routes ─────────────────────────────────────────────────────────
691
+ async function loadRoutes() {
692
+ el.reloadBtn.querySelector('svg').style.animation = 'spin .6s linear infinite';
693
+ try {
694
+ const res = await fetch('/' + CONFIG.path + '/routes');
695
+ S.routes = await res.json();
696
+ applyFilter(); renderRouteList();
697
+ } catch {
698
+ el.routeList.innerHTML = '<div class="empty-routes">⚠️ Could not load routes.<br/>Is the server running?</div>';
699
+ } finally {
700
+ el.reloadBtn.querySelector('svg').style.animation = '';
701
+ }
702
+ }
703
+
704
+ function applyFilter() {
705
+ const q = el.search.value.trim().toLowerCase();
706
+ // Exclude the explorer's own routes
707
+ const explorerPrefix = '/' + CONFIG.path;
708
+ const base = S.routes.filter(r => !r.path.startsWith(explorerPrefix));
709
+ S.filtered = q ? base.filter(r => r.path.toLowerCase().includes(q) || r.method.toLowerCase().includes(q)) : base;
710
+ }
711
+
712
+ function renderRouteList() {
713
+ if (!S.filtered.length) {
714
+ el.routeList.innerHTML = \`<div class="empty-routes">\${S.routes.length ? 'No routes match your search.' : 'No routes found.'}</div>\`;
715
+ return;
716
+ }
717
+ el.routeList.innerHTML = \`<div class="route-count">\${S.filtered.length} route\${S.filtered.length !== 1 ? 's' : ''}</div>\` +
718
+ S.filtered.map(r => {
719
+ const active = S.selected?.method === r.method && S.selected?.path === r.path ? ' active' : '';
720
+ return \`<div class="route-item\${active}" data-method="\${r.method}" data-path="\${esc(r.path)}">
721
+ <span class="method-badge method-\${r.method}">\${r.method}</span>
722
+ <span class="route-path" title="\${esc(r.path)}">\${esc(r.path)}</span>
723
+ </div>\`;
724
+ }).join('');
725
+ el.routeList.querySelectorAll('.route-item').forEach(item => {
726
+ item.addEventListener('click', () => {
727
+ const route = S.routes.find(r => r.method === item.dataset.method && r.path === item.dataset.path);
728
+ if (route) selectRoute(route);
729
+ });
730
+ });
731
+ }
732
+
733
+ function selectRoute(route) {
734
+ S.selected = route;
735
+ S.pathParams = {};
736
+ (route.params || []).forEach(p => { S.pathParams[p] = ''; });
737
+ S.response = null;
738
+ renderResponse(); renderUrlBar(); renderPathParams(); renderRouteList();
739
+ el.sendBtn.disabled = false;
740
+ el.methodPill.style.visibility = 'visible';
741
+
742
+ // Auto-switch to Params tab (always — it shows both path & query params)
743
+ el.reqTabBar.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
744
+ el.reqTabBar.querySelector('[data-tab="params"]').classList.add('active');
745
+ document.querySelectorAll('#req-panel-inner .tab-panel').forEach(p => p.classList.remove('active'));
746
+ document.getElementById('tab-params').classList.add('active');
747
+
748
+ // Auto-switch to Body tab for methods that send a body
749
+ const bodyMethods = ['POST', 'PUT', 'PATCH'];
750
+ if (bodyMethods.includes(route.method) && !route.params.length) {
751
+ el.reqTabBar.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
752
+ el.reqTabBar.querySelector('[data-tab="body"]').classList.add('active');
753
+ document.querySelectorAll('#req-panel-inner .tab-panel').forEach(p => p.classList.remove('active'));
754
+ document.getElementById('tab-body').classList.add('active');
755
+ }
756
+ }
757
+
758
+ // ── URL ────────────────────────────────────────────────────────────
759
+ function buildUrl() {
760
+ if (!S.selected) return '';
761
+ const raw = el.baseUrl.value;
762
+ const base = raw.endsWith('/') ? raw.slice(0, -1) : raw;
763
+ let path = S.selected.path;
764
+ Object.entries(S.pathParams).forEach(([k, v]) => {
765
+ path = path.replace(':' + k, encodeURIComponent(v || (':' + k)));
766
+ });
767
+ const qp = [...S.queryParams.filter(p => p.enabled && p.key).map(p => [p.key, p.value]),
768
+ ...(S.auth.type === 'apikey' && S.auth.apiKeyIn === 'query' && S.auth.apiKeyValue ? [[S.auth.apiKeyName, S.auth.apiKeyValue]] : [])];
769
+ const qs = qp.map(([k, v]) => encodeURIComponent(k) + '=' + encodeURIComponent(v)).join('&');
770
+ return base + path + (qs ? '?' + qs : '');
771
+ }
772
+
773
+ function renderUrlBar() {
774
+ if (!S.selected) return;
775
+ el.methodPill.textContent = S.selected.method;
776
+ el.methodPill.className = 'method-badge method-' + S.selected.method;
777
+ el.urlDisplay.value = buildUrl();
778
+ }
779
+
780
+ // ── Params / Headers ───────────────────────────────────────────────
781
+ function renderPathParams() {
782
+ const params = S.selected?.params || [];
783
+ el.pathParamsSec.classList.toggle('hidden', !params.length);
784
+ if (!params.length) return;
785
+ el.pathParamsRows.innerHTML = params.map(p => \`
786
+ <div class="param-row">
787
+ <span class="param-name">:\${esc(p)}</span>
788
+ <input class="kv-input" data-path-param="\${esc(p)}" placeholder="value" value="\${esc(S.pathParams[p] || '')}" spellcheck="false" />
789
+ </div>\`).join('');
790
+ el.pathParamsRows.querySelectorAll('[data-path-param]').forEach(input => {
791
+ input.addEventListener('input', () => { S.pathParams[input.dataset.pathParam] = input.value; renderUrlBar(); });
792
+ });
793
+ }
794
+
795
+ function renderQueryParams() {
796
+ el.queryParamsRows.innerHTML = S.queryParams.map(p => \`
797
+ <div class="kv-row" data-id="\${p.id}">
798
+ <input type="checkbox" class="kv-checkbox" \${p.enabled ? 'checked' : ''} data-action="toggle-query" data-id="\${p.id}" />
799
+ <input class="kv-input" placeholder="key" value="\${esc(p.key)}" data-action="key-query" data-id="\${p.id}" spellcheck="false" />
800
+ <input class="kv-input" placeholder="value" value="\${esc(p.value)}" data-action="val-query" data-id="\${p.id}" spellcheck="false" />
801
+ <button class="kv-remove" data-action="remove-query" data-id="\${p.id}">×</button>
802
+ </div>\`).join('');
803
+ el.queryParamsRows.querySelectorAll('[data-action]').forEach(bindKvAction);
804
+ }
805
+
806
+ function renderReqHeaders() {
807
+ el.reqHeadersRows.innerHTML = S.reqHeaders.map(h => \`
808
+ <div class="kv-row" data-id="\${h.id}">
809
+ <input type="checkbox" class="kv-checkbox" \${h.enabled ? 'checked' : ''} data-action="toggle-header" data-id="\${h.id}" />
810
+ <input class="kv-input" placeholder="Header-Name" value="\${esc(h.key)}" data-action="key-header" data-id="\${h.id}" spellcheck="false" />
811
+ <input class="kv-input" placeholder="value" value="\${esc(h.value)}" data-action="val-header" data-id="\${h.id}" spellcheck="false" />
812
+ <button class="kv-remove" data-action="remove-header" data-id="\${h.id}">×</button>
813
+ </div>\`).join('');
814
+ el.reqHeadersRows.querySelectorAll('[data-action]').forEach(bindKvAction);
815
+ }
816
+
817
+ function bindKvAction(node) {
818
+ const { action, id } = node.dataset; const pid = +id;
819
+ const handlers = {
820
+ 'toggle-query': () => { const p = S.queryParams.find(x=>x.id===pid); if(p){p.enabled=node.checked;renderUrlBar();} },
821
+ 'key-query': () => { const p = S.queryParams.find(x=>x.id===pid); if(p){p.key=node.value;renderUrlBar();} },
822
+ 'val-query': () => { const p = S.queryParams.find(x=>x.id===pid); if(p){p.value=node.value;renderUrlBar();} },
823
+ 'remove-query': () => { S.queryParams=S.queryParams.filter(x=>x.id!==pid); renderQueryParams(); renderUrlBar(); },
824
+ 'toggle-header': () => { const h = S.reqHeaders.find(x=>x.id===pid); if(h) h.enabled=node.checked; },
825
+ 'key-header': () => { const h = S.reqHeaders.find(x=>x.id===pid); if(h) h.key=node.value; },
826
+ 'val-header': () => { const h = S.reqHeaders.find(x=>x.id===pid); if(h) h.value=node.value; },
827
+ 'remove-header': () => { S.reqHeaders=S.reqHeaders.filter(x=>x.id!==pid); renderReqHeaders(); },
828
+ };
829
+ const evt = action.startsWith('toggle') || action.startsWith('remove') ? 'change' : 'input';
830
+ if (action.startsWith('remove')) node.addEventListener('click', handlers[action]);
831
+ else node.addEventListener(evt, handlers[action]);
832
+ }
833
+
834
+ // ── Send ───────────────────────────────────────────────────────────
835
+ async function sendRequest() {
836
+ if (!S.selected || S.loading) return;
837
+ S.loading = true;
838
+ el.sendBtn.innerHTML = '<span class="spinner"></span>';
839
+ el.sendBtn.disabled = true;
840
+
841
+ const headers = {};
842
+ S.reqHeaders.filter(h => h.enabled && h.key).forEach(h => { headers[h.key] = h.value; });
843
+ if (S.auth.type === 'bearer' && S.auth.bearerToken) headers['Authorization'] = 'Bearer ' + S.auth.bearerToken;
844
+ else if (S.auth.type === 'basic') headers['Authorization'] = 'Basic ' + btoa(S.auth.basicUsername + ':' + S.auth.basicPassword);
845
+ else if (S.auth.type === 'apikey' && S.auth.apiKeyIn === 'header' && S.auth.apiKeyValue) headers[S.auth.apiKeyName || 'X-API-Key'] = S.auth.apiKeyValue;
846
+ if (S.body.type === 'json') headers['Content-Type'] = 'application/json';
847
+ else if (S.body.type === 'form') headers['Content-Type'] = 'application/x-www-form-urlencoded';
848
+ else if (S.body.type === 'text') headers['Content-Type'] = 'text/plain';
849
+
850
+ const method = S.selected.method;
851
+ const noBody = ['GET','HEAD','DELETE','OPTIONS'].includes(method);
852
+ const body = (!noBody && S.body.type !== 'none') ? S.body.content || null : null;
853
+
854
+ const t0 = performance.now();
855
+ try {
856
+ const res = await fetch(buildUrl(), { method, headers, ...(body ? {body} : {}) });
857
+ const elapsed = Math.round(performance.now() - t0);
858
+ const text = await res.text();
859
+ const resHeaders = {};
860
+ res.headers.forEach((v, k) => { resHeaders[k] = v; });
861
+ const bytes = new TextEncoder().encode(text).length;
862
+ S.response = { status: res.status, statusText: res.statusText, time: elapsed, size: bytes < 1024 ? bytes + ' B' : (bytes/1024).toFixed(1) + ' KB', body: text, headers: resHeaders };
863
+ } catch(err) {
864
+ S.response = { status: 0, statusText: 'Network Error', time: Math.round(performance.now()-t0), size: '—', body: String(err), headers: {}, error: true };
865
+ } finally {
866
+ S.loading = false; el.sendBtn.innerHTML = 'Send'; el.sendBtn.disabled = false;
867
+ renderResponse();
868
+ }
869
+ }
870
+
871
+ // ── Response ───────────────────────────────────────────────────────
872
+ function renderResponse() {
873
+ const r = S.response;
874
+ if (!r) {
875
+ el.statusPill.textContent = ''; el.statusPill.className = 'status-pill';
876
+ el.resTime.textContent = ''; el.resSize.textContent = '';
877
+ el.copyBtn.classList.add('hidden');
878
+ el.resEmpty.classList.remove('hidden'); el.resBody.classList.add('hidden');
879
+ el.resHeadersList.innerHTML = '';
880
+ el.resHeadersCount.style.display = 'none';
881
+ return;
882
+ }
883
+ const sc = r.status;
884
+ let cls = 'status-pill ';
885
+ if (r.error||sc===0) cls+='status-err';
886
+ else if(sc<300) cls+='status-2xx';
887
+ else if(sc<400) cls+='status-3xx';
888
+ else if(sc<500) cls+='status-4xx';
889
+ else cls+='status-5xx';
890
+ el.statusPill.className = cls;
891
+ el.statusPill.textContent = r.error ? 'Error' : sc + ' ' + r.statusText;
892
+ el.resTime.textContent = r.time + ' ms';
893
+ el.resSize.textContent = r.size;
894
+ el.copyBtn.classList.remove('hidden');
895
+ el.resEmpty.classList.add('hidden'); el.resBody.classList.remove('hidden');
896
+
897
+ try {
898
+ const parsed = JSON.parse(r.body);
899
+ el.resBody.innerHTML = '<pre class="json-body">' + syntaxHighlight(JSON.stringify(parsed, null, 2)) + '</pre>';
900
+ } catch {
901
+ el.resBody.innerHTML = '<pre class="raw-body">' + esc(r.body) + '</pre>';
902
+ }
903
+
904
+ const hEntries = Object.entries(r.headers);
905
+ if (hEntries.length) {
906
+ el.resHeadersCount.textContent = hEntries.length;
907
+ el.resHeadersCount.style.display = 'inline-flex';
908
+ } else {
909
+ el.resHeadersCount.style.display = 'none';
910
+ }
911
+ el.resHeadersList.innerHTML = hEntries.map(([k,v]) =>
912
+ \`<div class="res-header-row"><span class="res-header-key">\${esc(k)}</span><span class="res-header-val">\${esc(v)}</span></div>\`
913
+ ).join('');
914
+ }
915
+
916
+ function syntaxHighlight(json) {
917
+ return json.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
918
+ .replace(/("(?:\\\\u[\\da-fA-F]{4}|\\\\[^u]|[^\\\\\\"])*"(?:\\s*:)?|\\b(?:true|false|null)\\b|-?\\d+(?:\\.\\d+)?(?:[eE][+\\-]?\\d+)?)/g, m => {
919
+ let c = 'json-number';
920
+ if(/^"/.test(m)) c = /:$/.test(m) ? 'json-key' : 'json-string';
921
+ else if(/true|false/.test(m)) c = 'json-boolean';
922
+ else if(/null/.test(m)) c = 'json-null';
923
+ return \`<span class="\${c}">\${m}</span>\`;
924
+ });
925
+ }
926
+
927
+ function esc(s) {
928
+ return String(s??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
929
+ }
930
+
931
+ // ── Tabs ───────────────────────────────────────────────────────────
932
+ function initTabs(barEl, prefix) {
933
+ barEl.querySelectorAll('.tab-btn').forEach(btn => {
934
+ btn.addEventListener('click', () => {
935
+ barEl.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
936
+ btn.classList.add('active');
937
+ const tab = btn.dataset.tab;
938
+ if (prefix === 'req') {
939
+ document.querySelectorAll('#req-panel-inner .tab-panel').forEach(p => p.classList.remove('active'));
940
+ const panel = $('tab-' + tab);
941
+ if (panel) panel.classList.add('active');
942
+ } else {
943
+ $('res-body-panel').classList.toggle('hidden', tab !== 'body');
944
+ $('res-headers-panel').classList.toggle('hidden', tab !== 'headers');
945
+ }
946
+ });
947
+ });
948
+ }
949
+
950
+ // ── Resize ─────────────────────────────────────────────────────────
951
+ function initPanelResize() {
952
+ let drag=false, startY=0, startH=0;
953
+ el.resizeHandle.addEventListener('mousedown', e => {
954
+ drag=true; startY=e.clientY; startH=el.reqPanel.offsetHeight;
955
+ el.resizeHandle.classList.add('dragging');
956
+ document.body.style.cssText += ';cursor:row-resize;user-select:none';
957
+ e.preventDefault();
958
+ });
959
+ document.addEventListener('mousemove', e => {
960
+ if(!drag) return;
961
+ const maxH = el.panels.offsetHeight - 148;
962
+ const h = Math.max(120, Math.min(startH + e.clientY - startY, maxH));
963
+ el.reqPanel.style.flex = '0 0 ' + h + 'px';
964
+ });
965
+ document.addEventListener('mouseup', () => {
966
+ if(!drag) return; drag=false;
967
+ el.resizeHandle.classList.remove('dragging');
968
+ document.body.style.cursor=''; document.body.style.userSelect='';
969
+ });
970
+ }
971
+
972
+ function initSidebarResize() {
973
+ let drag=false, startX=0, startW=0;
974
+ el.sidebarResize.addEventListener('mousedown', e => {
975
+ drag=true; startX=e.clientX; startW=el.sidebar.offsetWidth;
976
+ el.sidebarResize.classList.add('dragging');
977
+ document.body.style.cssText += 'cursor:col-resize;user-select:none';
978
+ });
979
+ document.addEventListener('mousemove', e => {
980
+ if(!drag) return;
981
+ el.sidebar.style.width = Math.max(160, Math.min(startW + e.clientX - startX, 480)) + 'px';
982
+ });
983
+ document.addEventListener('mouseup', () => {
984
+ if(!drag) return; drag=false;
985
+ el.sidebarResize.classList.remove('dragging');
986
+ document.body.style.cursor=''; document.body.style.userSelect='';
987
+ });
988
+ }
989
+
990
+ // ── Events ─────────────────────────────────────────────────────────
991
+ function bindEvents() {
992
+ el.reloadBtn.addEventListener('click', loadRoutes);
993
+ el.search.addEventListener('input', () => { applyFilter(); renderRouteList(); });
994
+ el.baseUrl.addEventListener('input', renderUrlBar);
995
+ el.sendBtn.addEventListener('click', sendRequest);
996
+
997
+ el.addQueryBtn.addEventListener('click', () => {
998
+ S.queryParams.push({ id: uid(), key: '', value: '', enabled: true });
999
+ renderQueryParams();
1000
+ });
1001
+ el.addHeaderBtn.addEventListener('click', () => {
1002
+ S.reqHeaders.push({ id: uid(), key: '', value: '', enabled: true });
1003
+ renderReqHeaders();
1004
+ });
1005
+
1006
+ document.querySelectorAll('.auth-type-btn').forEach(btn => {
1007
+ btn.addEventListener('click', () => {
1008
+ document.querySelectorAll('.auth-type-btn').forEach(b => b.classList.remove('active'));
1009
+ btn.classList.add('active'); S.auth.type = btn.dataset.auth;
1010
+ ['none','bearer','basic','apikey'].forEach(t => {
1011
+ $('auth-'+t+'-panel').classList.toggle('hidden', t !== S.auth.type);
1012
+ });
1013
+ });
1014
+ });
1015
+ el.bearerToken.addEventListener('input', () => { S.auth.bearerToken = el.bearerToken.value; });
1016
+ el.basicUsername.addEventListener('input', () => { S.auth.basicUsername = el.basicUsername.value; });
1017
+ el.basicPassword.addEventListener('input', () => { S.auth.basicPassword = el.basicPassword.value; });
1018
+ el.apikeyName.addEventListener('input', () => { S.auth.apiKeyName = el.apikeyName.value; });
1019
+ el.apikeyValue.addEventListener('input', () => { S.auth.apiKeyValue = el.apikeyValue.value; });
1020
+ el.apikeyIn.addEventListener('change', () => { S.auth.apiKeyIn = el.apikeyIn.value; renderUrlBar(); });
1021
+
1022
+ document.querySelectorAll('.body-type-btn').forEach(btn => {
1023
+ btn.addEventListener('click', () => {
1024
+ document.querySelectorAll('.body-type-btn').forEach(b => b.classList.remove('active'));
1025
+ btn.classList.add('active'); S.body.type = btn.dataset.body;
1026
+ const show = S.body.type !== 'none';
1027
+ el.bodyEditorWrap.classList.toggle('hidden', !show);
1028
+ el.bodyNoneMsg.classList.toggle('hidden', show);
1029
+ el.formatBtn.style.display = S.body.type === 'json' ? '' : 'none';
1030
+ });
1031
+ });
1032
+ el.bodyEditor.addEventListener('input', () => { S.body.content = el.bodyEditor.value; });
1033
+ el.formatBtn.addEventListener('click', () => {
1034
+ try {
1035
+ el.bodyEditor.value = JSON.stringify(JSON.parse(el.bodyEditor.value), null, 2);
1036
+ S.body.content = el.bodyEditor.value;
1037
+ } catch {
1038
+ el.bodyEditor.style.borderColor = '#ef4444';
1039
+ setTimeout(() => { el.bodyEditor.style.borderColor = ''; }, 800);
1040
+ }
1041
+ });
1042
+ el.copyBtn.addEventListener('click', () => {
1043
+ if (!S.response) return;
1044
+ navigator.clipboard.writeText(S.response.body).then(() => {
1045
+ const o = el.copyBtn.innerHTML;
1046
+ el.copyBtn.textContent = '✓ Copied';
1047
+ setTimeout(() => { el.copyBtn.innerHTML = o; }, 1500);
1048
+ });
1049
+ });
1050
+
1051
+ document.addEventListener('keydown', e => {
1052
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter' && !el.sendBtn.disabled) sendRequest();
1053
+ });
1054
+
1055
+ initTabs(el.reqTabBar, 'req');
1056
+ initTabs(el.resTabBar, 'res');
1057
+ initPanelResize();
1058
+ initSidebarResize();
1059
+ }
1060
+
1061
+ init();
1062
+ </script>
1063
+ </body>
1064
+ </html>`;
1065
+ }
1066
+ //# sourceMappingURL=api-explorer.html.js.map