@engjts/nexus 0.1.8 → 0.1.9

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 (205) hide show
  1. package/package.json +1 -1
  2. package/BENCHMARK_REPORT.md +0 -343
  3. package/documentation/01-getting-started.md +0 -240
  4. package/documentation/02-context.md +0 -335
  5. package/documentation/03-routing.md +0 -397
  6. package/documentation/04-middleware.md +0 -483
  7. package/documentation/05-validation.md +0 -514
  8. package/documentation/06-error-handling.md +0 -465
  9. package/documentation/07-performance.md +0 -364
  10. package/documentation/08-adapters.md +0 -470
  11. package/documentation/09-api-reference.md +0 -548
  12. package/documentation/10-examples.md +0 -582
  13. package/documentation/11-deployment.md +0 -477
  14. package/documentation/12-sentry.md +0 -620
  15. package/documentation/13-sentry-data-storage.md +0 -996
  16. package/documentation/14-sentry-data-reference.md +0 -457
  17. package/documentation/15-sentry-summary.md +0 -409
  18. package/documentation/16-alerts-system.md +0 -745
  19. package/documentation/17-alert-adapters.md +0 -696
  20. package/documentation/18-alerts-implementation-summary.md +0 -385
  21. package/documentation/19-class-based-routing.md +0 -840
  22. package/documentation/20-websocket-realtime.md +0 -813
  23. package/documentation/21-cache-system.md +0 -510
  24. package/documentation/22-job-queue.md +0 -772
  25. package/documentation/23-sentry-plugin.md +0 -551
  26. package/documentation/24-testing-utilities.md +0 -1287
  27. package/documentation/25-api-versioning.md +0 -533
  28. package/documentation/26-context-store.md +0 -607
  29. package/documentation/27-dependency-injection.md +0 -329
  30. package/documentation/28-lifecycle-hooks.md +0 -521
  31. package/documentation/29-package-structure.md +0 -196
  32. package/documentation/30-plugin-system.md +0 -414
  33. package/documentation/31-jwt-authentication.md +0 -597
  34. package/documentation/32-cli.md +0 -268
  35. package/documentation/ALERTS-COMPLETE-SUMMARY.md +0 -429
  36. package/documentation/ALERTS-INDEX.md +0 -330
  37. package/documentation/ALERTS-QUICK-REFERENCE.md +0 -286
  38. package/documentation/README.md +0 -178
  39. package/documentation/index.html +0 -34
  40. package/modern_framework_paper.md +0 -1870
  41. package/public/css/style.css +0 -87
  42. package/public/index.html +0 -34
  43. package/public/js/app.js +0 -27
  44. package/src/advanced/cache/InMemoryCacheStore.ts +0 -68
  45. package/src/advanced/cache/MultiTierCache.ts +0 -194
  46. package/src/advanced/cache/RedisCacheStore.ts +0 -341
  47. package/src/advanced/cache/index.ts +0 -5
  48. package/src/advanced/cache/types.ts +0 -40
  49. package/src/advanced/graphql/SimpleDataLoader.ts +0 -42
  50. package/src/advanced/graphql/index.ts +0 -22
  51. package/src/advanced/graphql/server.ts +0 -252
  52. package/src/advanced/graphql/types.ts +0 -42
  53. package/src/advanced/jobs/InMemoryQueueStore.ts +0 -68
  54. package/src/advanced/jobs/JobQueue.ts +0 -556
  55. package/src/advanced/jobs/RedisQueueStore.ts +0 -367
  56. package/src/advanced/jobs/index.ts +0 -5
  57. package/src/advanced/jobs/types.ts +0 -70
  58. package/src/advanced/observability/APMManager.ts +0 -163
  59. package/src/advanced/observability/AlertManager.ts +0 -109
  60. package/src/advanced/observability/MetricRegistry.ts +0 -151
  61. package/src/advanced/observability/ObservabilityCenter.ts +0 -304
  62. package/src/advanced/observability/StructuredLogger.ts +0 -154
  63. package/src/advanced/observability/TracingManager.ts +0 -117
  64. package/src/advanced/observability/adapters.ts +0 -304
  65. package/src/advanced/observability/createObservabilityMiddleware.ts +0 -63
  66. package/src/advanced/observability/index.ts +0 -11
  67. package/src/advanced/observability/types.ts +0 -174
  68. package/src/advanced/playground/extractPathParams.ts +0 -6
  69. package/src/advanced/playground/generateFieldExample.ts +0 -31
  70. package/src/advanced/playground/generatePlaygroundHTML.ts +0 -1956
  71. package/src/advanced/playground/generateSummary.ts +0 -19
  72. package/src/advanced/playground/getTagFromPath.ts +0 -9
  73. package/src/advanced/playground/index.ts +0 -8
  74. package/src/advanced/playground/playground.ts +0 -250
  75. package/src/advanced/playground/types.ts +0 -49
  76. package/src/advanced/playground/zodToExample.ts +0 -16
  77. package/src/advanced/playground/zodToParams.ts +0 -15
  78. package/src/advanced/postman/buildAuth.ts +0 -31
  79. package/src/advanced/postman/buildBody.ts +0 -15
  80. package/src/advanced/postman/buildQueryParams.ts +0 -27
  81. package/src/advanced/postman/buildRequestItem.ts +0 -36
  82. package/src/advanced/postman/buildResponses.ts +0 -11
  83. package/src/advanced/postman/buildUrl.ts +0 -33
  84. package/src/advanced/postman/capitalize.ts +0 -4
  85. package/src/advanced/postman/generateCollection.ts +0 -59
  86. package/src/advanced/postman/generateEnvironment.ts +0 -34
  87. package/src/advanced/postman/generateExampleFromZod.ts +0 -21
  88. package/src/advanced/postman/generateFieldExample.ts +0 -45
  89. package/src/advanced/postman/generateName.ts +0 -20
  90. package/src/advanced/postman/generateUUID.ts +0 -11
  91. package/src/advanced/postman/getTagFromPath.ts +0 -10
  92. package/src/advanced/postman/index.ts +0 -28
  93. package/src/advanced/postman/postman.ts +0 -156
  94. package/src/advanced/postman/slugify.ts +0 -7
  95. package/src/advanced/postman/types.ts +0 -140
  96. package/src/advanced/realtime/index.ts +0 -18
  97. package/src/advanced/realtime/websocket.ts +0 -231
  98. package/src/advanced/sentry/index.ts +0 -1236
  99. package/src/advanced/sentry/types.ts +0 -355
  100. package/src/advanced/static/generateDirectoryListing.ts +0 -47
  101. package/src/advanced/static/generateETag.ts +0 -7
  102. package/src/advanced/static/getMimeType.ts +0 -9
  103. package/src/advanced/static/index.ts +0 -32
  104. package/src/advanced/static/isSafePath.ts +0 -13
  105. package/src/advanced/static/publicDir.ts +0 -21
  106. package/src/advanced/static/serveStatic.ts +0 -225
  107. package/src/advanced/static/spa.ts +0 -24
  108. package/src/advanced/static/types.ts +0 -159
  109. package/src/advanced/swagger/SwaggerGenerator.ts +0 -66
  110. package/src/advanced/swagger/buildOperation.ts +0 -61
  111. package/src/advanced/swagger/buildParameters.ts +0 -61
  112. package/src/advanced/swagger/buildRequestBody.ts +0 -21
  113. package/src/advanced/swagger/buildResponses.ts +0 -54
  114. package/src/advanced/swagger/capitalize.ts +0 -5
  115. package/src/advanced/swagger/convertPath.ts +0 -9
  116. package/src/advanced/swagger/createSwagger.ts +0 -12
  117. package/src/advanced/swagger/generateOperationId.ts +0 -21
  118. package/src/advanced/swagger/generateSpec.ts +0 -105
  119. package/src/advanced/swagger/generateSummary.ts +0 -24
  120. package/src/advanced/swagger/generateSwaggerUI.ts +0 -70
  121. package/src/advanced/swagger/generateThemeCss.ts +0 -53
  122. package/src/advanced/swagger/index.ts +0 -25
  123. package/src/advanced/swagger/swagger.ts +0 -237
  124. package/src/advanced/swagger/types.ts +0 -206
  125. package/src/advanced/swagger/zodFieldToOpenAPI.ts +0 -94
  126. package/src/advanced/swagger/zodSchemaToOpenAPI.ts +0 -50
  127. package/src/advanced/swagger/zodToOpenAPI.ts +0 -22
  128. package/src/advanced/testing/factory.ts +0 -509
  129. package/src/advanced/testing/harness.ts +0 -612
  130. package/src/advanced/testing/index.ts +0 -430
  131. package/src/advanced/testing/load-test.ts +0 -618
  132. package/src/advanced/testing/mock-server.ts +0 -498
  133. package/src/advanced/testing/mock.ts +0 -670
  134. package/src/cli/bin.ts +0 -9
  135. package/src/cli/cli.ts +0 -158
  136. package/src/cli/commands/add.ts +0 -178
  137. package/src/cli/commands/build.ts +0 -73
  138. package/src/cli/commands/create.ts +0 -166
  139. package/src/cli/commands/dev.ts +0 -85
  140. package/src/cli/commands/generate.ts +0 -99
  141. package/src/cli/commands/help.ts +0 -95
  142. package/src/cli/commands/init.ts +0 -91
  143. package/src/cli/commands/version.ts +0 -38
  144. package/src/cli/index.ts +0 -6
  145. package/src/cli/templates/generators.ts +0 -359
  146. package/src/cli/templates/index.ts +0 -680
  147. package/src/cli/utils/exec.ts +0 -52
  148. package/src/cli/utils/file-system.ts +0 -78
  149. package/src/cli/utils/logger.ts +0 -111
  150. package/src/core/adapter.ts +0 -88
  151. package/src/core/application.ts +0 -1453
  152. package/src/core/context-pool.ts +0 -79
  153. package/src/core/context.ts +0 -856
  154. package/src/core/index.ts +0 -94
  155. package/src/core/middleware.ts +0 -272
  156. package/src/core/performance/buffer-pool.ts +0 -108
  157. package/src/core/performance/middleware-optimizer.ts +0 -162
  158. package/src/core/plugin/PluginManager.ts +0 -435
  159. package/src/core/plugin/builder.ts +0 -358
  160. package/src/core/plugin/index.ts +0 -50
  161. package/src/core/plugin/types.ts +0 -214
  162. package/src/core/router/file-router.ts +0 -623
  163. package/src/core/router/index.ts +0 -260
  164. package/src/core/router/radix-tree.ts +0 -242
  165. package/src/core/serializer.ts +0 -397
  166. package/src/core/store/index.ts +0 -30
  167. package/src/core/store/registry.ts +0 -178
  168. package/src/core/store/request-store.ts +0 -240
  169. package/src/core/store/types.ts +0 -233
  170. package/src/core/types.ts +0 -616
  171. package/src/database/adapter.ts +0 -35
  172. package/src/database/adapters/index.ts +0 -1
  173. package/src/database/adapters/mysql.ts +0 -669
  174. package/src/database/database.ts +0 -70
  175. package/src/database/dialect.ts +0 -388
  176. package/src/database/index.ts +0 -12
  177. package/src/database/migrations.ts +0 -86
  178. package/src/database/optimizer.ts +0 -125
  179. package/src/database/query-builder.ts +0 -404
  180. package/src/database/realtime.ts +0 -53
  181. package/src/database/schema.ts +0 -71
  182. package/src/database/transactions.ts +0 -56
  183. package/src/database/types.ts +0 -87
  184. package/src/deployment/cluster.ts +0 -471
  185. package/src/deployment/config.ts +0 -454
  186. package/src/deployment/docker.ts +0 -599
  187. package/src/deployment/graceful-shutdown.ts +0 -373
  188. package/src/deployment/index.ts +0 -56
  189. package/src/index.ts +0 -281
  190. package/src/security/adapter.ts +0 -318
  191. package/src/security/auth/JWTPlugin.ts +0 -234
  192. package/src/security/auth/JWTProvider.ts +0 -316
  193. package/src/security/auth/adapter.ts +0 -12
  194. package/src/security/auth/jwt.ts +0 -234
  195. package/src/security/auth/middleware.ts +0 -188
  196. package/src/security/csrf.ts +0 -220
  197. package/src/security/headers.ts +0 -108
  198. package/src/security/index.ts +0 -60
  199. package/src/security/rate-limit/adapter.ts +0 -7
  200. package/src/security/rate-limit/memory.ts +0 -108
  201. package/src/security/rate-limit/middleware.ts +0 -181
  202. package/src/security/sanitization.ts +0 -75
  203. package/src/security/types.ts +0 -240
  204. package/src/security/utils.ts +0 -52
  205. package/tsconfig.json +0 -39
@@ -1,1956 +0,0 @@
1
- import { PlaygroundConfig } from './types';
2
-
3
-
4
-
5
- export function generatePlaygroundHTML(config: PlaygroundConfig, baseUrl: string): string {
6
- const isDark = config.theme === 'dark';
7
-
8
- return `<!DOCTYPE html>
9
- <html lang="en">
10
- <head>
11
- <meta charset="UTF-8">
12
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
13
- <title>${config.title}</title>
14
- <link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'></text></svg>">
15
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs/editor/editor.main.css">
16
-
17
- <style>
18
- :root {
19
- --bg-primary: ${isDark ? '#0d1117' : '#ffffff'};
20
- --bg-secondary: ${isDark ? '#161b22' : '#f6f8fa'};
21
- --bg-tertiary: ${isDark ? '#21262d' : '#eaeef2'};
22
- --text-primary: ${isDark ? '#e6edf3' : '#1f2328'};
23
- --text-secondary: ${isDark ? '#8b949e' : '#656d76'};
24
- --text-muted: ${isDark ? '#6e7681' : '#8c959f'};
25
- --border-color: ${isDark ? '#30363d' : '#d0d7de'};
26
- --accent-color: #2f81f7;
27
- --accent-hover: #1f6feb;
28
- --success-color: #3fb950;
29
- --error-color: #f85149;
30
- --method-get: #3fb950;
31
- --method-post: #2f81f7;
32
- --method-put: #d29922;
33
- --method-patch: #a371f7;
34
- --method-delete: #f85149;
35
- --sidebar-width: 320px;
36
- }
37
-
38
- * { margin: 0; padding: 0; box-sizing: border-box; }
39
-
40
- body {
41
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
42
- background: var(--bg-primary);
43
- color: var(--text-primary);
44
- height: 100vh;
45
- overflow: hidden;
46
- }
47
-
48
- .app { display: flex; height: 100vh; }
49
-
50
- /* Sidebar */
51
- .sidebar {
52
- width: var(--sidebar-width);
53
- min-width: 60px;
54
- background: var(--bg-secondary);
55
- border-right: 1px solid var(--border-color);
56
- display: flex;
57
- flex-direction: column;
58
- transition: width 0.3s ease;
59
- position: relative;
60
- }
61
-
62
- .sidebar.collapsed {
63
- width: 60px;
64
- }
65
-
66
- .sidebar.collapsed .sidebar-content { display: none; }
67
- .sidebar.collapsed .collapse-btn { transform: rotate(180deg); }
68
-
69
- .sidebar-header {
70
- padding: 12px 16px;
71
- border-bottom: 1px solid var(--border-color);
72
- display: flex;
73
- align-items: center;
74
- gap: 10px;
75
- min-height: 52px;
76
- }
77
-
78
- .sidebar-header h1 {
79
- font-size: 15px;
80
- font-weight: 600;
81
- white-space: nowrap;
82
- overflow: hidden;
83
- }
84
-
85
- .sidebar.collapsed .sidebar-header h1 { display: none; }
86
-
87
- .logo { font-size: 22px; flex-shrink: 0; }
88
-
89
- .collapse-btn {
90
- margin-left: auto;
91
- padding: 6px;
92
- border: none;
93
- background: transparent;
94
- color: var(--text-secondary);
95
- cursor: pointer;
96
- border-radius: 4px;
97
- transition: all 0.2s;
98
- font-size: 16px;
99
- }
100
-
101
- .collapse-btn:hover { background: var(--bg-tertiary); color: var(--text-primary); }
102
-
103
- .sidebar-content { flex: 1; overflow: hidden; display: flex; flex-direction: column; }
104
-
105
- .search-box { padding: 12px; border-bottom: 1px solid var(--border-color); }
106
-
107
- .search-input {
108
- width: 100%;
109
- padding: 8px 12px;
110
- border: 1px solid var(--border-color);
111
- border-radius: 6px;
112
- background: var(--bg-primary);
113
- color: var(--text-primary);
114
- font-size: 13px;
115
- outline: none;
116
- }
117
-
118
- .search-input:focus { border-color: var(--accent-color); }
119
- .search-input::placeholder { color: var(--text-muted); }
120
-
121
- .endpoints-list { flex: 1; overflow-y: auto; padding: 8px 0; }
122
-
123
- .endpoint-group { margin-bottom: 2px; }
124
-
125
- .endpoint-group-header {
126
- padding: 8px 12px;
127
- font-size: 11px;
128
- font-weight: 600;
129
- color: var(--text-secondary);
130
- text-transform: uppercase;
131
- letter-spacing: 0.5px;
132
- cursor: pointer;
133
- display: flex;
134
- align-items: center;
135
- gap: 6px;
136
- user-select: none;
137
- }
138
-
139
- .endpoint-group-header:hover { background: var(--bg-tertiary); }
140
- .endpoint-group-header .arrow { transition: transform 0.2s; font-size: 10px; }
141
- .endpoint-group.collapsed .arrow { transform: rotate(-90deg); }
142
- .endpoint-group.collapsed .endpoint-group-items { display: none; }
143
-
144
- .endpoint-item {
145
- padding: 8px 12px 8px 20px;
146
- cursor: pointer;
147
- display: flex;
148
- align-items: center;
149
- gap: 8px;
150
- transition: background 0.15s;
151
- border-left: 3px solid transparent;
152
- }
153
-
154
- .endpoint-item:hover { background: var(--bg-tertiary); }
155
- .endpoint-item.active { background: var(--bg-tertiary); border-left-color: var(--accent-color); }
156
- .endpoint-item.deprecated { opacity: 0.5; text-decoration: line-through; }
157
-
158
- .method-badge {
159
- font-size: 9px;
160
- font-weight: 700;
161
- padding: 2px 5px;
162
- border-radius: 3px;
163
- text-transform: uppercase;
164
- min-width: 44px;
165
- text-align: center;
166
- }
167
-
168
- .method-badge.get { background: var(--method-get); color: #000; }
169
- .method-badge.post { background: var(--method-post); color: #fff; }
170
- .method-badge.put { background: var(--method-put); color: #000; }
171
- .method-badge.patch { background: var(--method-patch); color: #fff; }
172
- .method-badge.delete { background: var(--method-delete); color: #fff; }
173
-
174
- .endpoint-path {
175
- font-size: 12px;
176
- font-family: 'SF Mono', 'Fira Code', monospace;
177
- white-space: nowrap;
178
- overflow: hidden;
179
- text-overflow: ellipsis;
180
- }
181
-
182
- /* Main */
183
- .main { flex: 1; display: flex; flex-direction: column; min-width: 0; overflow: hidden; }
184
-
185
- /* Request Section */
186
- .request-section {
187
- display: flex;
188
- flex-direction: column;
189
- border-bottom: 1px solid var(--border-color);
190
- height: 30%;
191
- min-height: 150px;
192
- }
193
-
194
- .request-header {
195
- padding: 10px 16px;
196
- background: var(--bg-secondary);
197
- border-bottom: 1px solid var(--border-color);
198
- display: flex;
199
- align-items: center;
200
- gap: 10px;
201
- flex-shrink: 0;
202
- }
203
-
204
- .url-bar {
205
- flex: 1;
206
- display: flex;
207
- align-items: center;
208
- gap: 6px;
209
- background: var(--bg-primary);
210
- border: 1px solid var(--border-color);
211
- border-radius: 6px;
212
- padding: 3px;
213
- }
214
-
215
- .method-select {
216
- padding: 7px 10px;
217
- border: none;
218
- border-radius: 4px;
219
- font-size: 12px;
220
- font-weight: 600;
221
- cursor: pointer;
222
- outline: none;
223
- background: var(--method-get);
224
- color: #000;
225
- min-width: 80px;
226
- }
227
-
228
- .url-input {
229
- flex: 1;
230
- padding: 7px 10px;
231
- border: none;
232
- background: transparent;
233
- color: var(--text-primary);
234
- font-size: 13px;
235
- font-family: 'SF Mono', 'Fira Code', monospace;
236
- outline: none;
237
- }
238
-
239
- .send-btn {
240
- padding: 7px 16px;
241
- background: var(--accent-color);
242
- color: #fff;
243
- border: none;
244
- border-radius: 5px;
245
- font-size: 13px;
246
- font-weight: 600;
247
- cursor: pointer;
248
- display: flex;
249
- align-items: center;
250
- gap: 6px;
251
- transition: background 0.2s;
252
- }
253
-
254
- .send-btn:hover { background: var(--accent-hover); }
255
- .send-btn:disabled { opacity: 0.6; cursor: not-allowed; }
256
-
257
- .spinner {
258
- width: 12px;
259
- height: 12px;
260
- border: 2px solid rgba(255,255,255,0.3);
261
- border-top-color: #fff;
262
- border-radius: 50%;
263
- animation: spin 0.8s linear infinite;
264
- display: none;
265
- }
266
-
267
- @keyframes spin { to { transform: rotate(360deg); } }
268
-
269
- .tabs {
270
- display: flex;
271
- padding: 0 16px;
272
- background: var(--bg-secondary);
273
- border-bottom: 1px solid var(--border-color);
274
- flex-shrink: 0;
275
- }
276
-
277
- .tab {
278
- padding: 10px 14px;
279
- font-size: 12px;
280
- font-weight: 500;
281
- color: var(--text-secondary);
282
- cursor: pointer;
283
- border-bottom: 2px solid transparent;
284
- transition: all 0.2s;
285
- }
286
-
287
- .tab:hover { color: var(--text-primary); }
288
- .tab.active { color: var(--text-primary); border-bottom-color: var(--accent-color); }
289
-
290
- .tab-badge {
291
- margin-left: 4px;
292
- padding: 1px 5px;
293
- font-size: 10px;
294
- background: var(--bg-tertiary);
295
- border-radius: 8px;
296
- }
297
-
298
- .editor-container { flex: 1; display: flex; overflow: hidden; }
299
- .editor-wrapper { flex: 1; display: none; overflow: hidden; }
300
- .editor-wrapper.active { display: flex; flex-direction: column; }
301
- .editor { flex: 1; }
302
-
303
- .params-editor { padding: 12px 16px; overflow-y: auto; height: 100%; }
304
-
305
- .param-row {
306
- display: flex;
307
- align-items: center;
308
- gap: 8px;
309
- margin-bottom: 8px;
310
- }
311
-
312
- .param-checkbox { width: 16px; height: 16px; accent-color: var(--accent-color); }
313
-
314
- .param-input {
315
- flex: 1;
316
- padding: 8px 10px;
317
- border: 1px solid var(--border-color);
318
- border-radius: 5px;
319
- background: var(--bg-primary);
320
- color: var(--text-primary);
321
- font-size: 12px;
322
- font-family: 'SF Mono', 'Fira Code', monospace;
323
- outline: none;
324
- }
325
-
326
- .param-input:focus { border-color: var(--accent-color); }
327
- .param-input.key { max-width: 160px; }
328
-
329
- .param-delete {
330
- padding: 6px;
331
- border: none;
332
- background: transparent;
333
- color: var(--text-muted);
334
- cursor: pointer;
335
- border-radius: 3px;
336
- }
337
-
338
- .param-delete:hover { background: var(--error-color); color: #fff; }
339
-
340
- .add-param-btn {
341
- padding: 6px 12px;
342
- border: 1px dashed var(--border-color);
343
- background: transparent;
344
- color: var(--text-secondary);
345
- border-radius: 5px;
346
- cursor: pointer;
347
- font-size: 12px;
348
- }
349
-
350
- .add-param-btn:hover { border-color: var(--accent-color); color: var(--accent-color); }
351
-
352
- /* Info Panel */
353
- .info-panel {
354
- padding: 16px;
355
- overflow-y: auto;
356
- height: 100%;
357
- }
358
-
359
- .info-empty {
360
- color: var(--text-muted);
361
- font-size: 13px;
362
- text-align: center;
363
- padding: 24px;
364
- }
365
-
366
- .info-section {
367
- margin-bottom: 16px;
368
- }
369
-
370
- .info-section:last-child {
371
- margin-bottom: 0;
372
- }
373
-
374
- .info-label {
375
- font-size: 10px;
376
- font-weight: 600;
377
- color: var(--text-secondary);
378
- text-transform: uppercase;
379
- letter-spacing: 0.5px;
380
- margin-bottom: 6px;
381
- }
382
-
383
- .info-value {
384
- font-size: 13px;
385
- color: var(--text-primary);
386
- line-height: 1.5;
387
- }
388
-
389
- .info-tags {
390
- display: flex;
391
- flex-wrap: wrap;
392
- gap: 6px;
393
- }
394
-
395
- .info-tag {
396
- padding: 3px 8px;
397
- background: var(--accent-color);
398
- color: #fff;
399
- border-radius: 12px;
400
- font-size: 11px;
401
- font-weight: 500;
402
- }
403
-
404
- .info-deprecated {
405
- display: inline-flex;
406
- align-items: center;
407
- gap: 6px;
408
- padding: 6px 10px;
409
- background: rgba(248,81,73,0.15);
410
- color: var(--error-color);
411
- border-radius: 6px;
412
- font-size: 12px;
413
- font-weight: 500;
414
- }
415
-
416
- .info-responses {
417
- display: flex;
418
- flex-direction: column;
419
- gap: 6px;
420
- }
421
-
422
- .info-response-item {
423
- display: flex;
424
- align-items: center;
425
- gap: 10px;
426
- padding: 8px 10px;
427
- background: var(--bg-tertiary);
428
- border-radius: 6px;
429
- }
430
-
431
- .info-response-code {
432
- font-family: 'SF Mono', 'Fira Code', monospace;
433
- font-size: 12px;
434
- font-weight: 600;
435
- padding: 2px 6px;
436
- border-radius: 4px;
437
- min-width: 40px;
438
- text-align: center;
439
- }
440
-
441
- .info-response-code.success { background: rgba(63,185,80,0.2); color: var(--success-color); }
442
- .info-response-code.redirect { background: rgba(47,129,247,0.2); color: var(--accent-color); }
443
- .info-response-code.client-error { background: rgba(210,153,34,0.2); color: #d29922; }
444
- .info-response-code.server-error { background: rgba(248,81,73,0.2); color: var(--error-color); }
445
-
446
- .info-response-desc {
447
- font-size: 12px;
448
- color: var(--text-secondary);
449
- }
450
-
451
- .info-method-path {
452
- display: flex;
453
- align-items: center;
454
- gap: 10px;
455
- padding: 10px 12px;
456
- background: var(--bg-tertiary);
457
- border-radius: 6px;
458
- margin-bottom: 16px;
459
- }
460
-
461
- .info-method-path .method-badge {
462
- font-size: 11px;
463
- padding: 4px 8px;
464
- }
465
-
466
- .info-method-path .path {
467
- font-family: 'SF Mono', 'Fira Code', monospace;
468
- font-size: 13px;
469
- color: var(--text-primary);
470
- }
471
-
472
- .info-label-row {
473
- display: flex;
474
- align-items: center;
475
- justify-content: space-between;
476
- margin-bottom: 6px;
477
- }
478
-
479
- .info-label-row .info-label {
480
- margin-bottom: 0;
481
- }
482
-
483
- .info-use-btn {
484
- padding: 4px 10px;
485
- background: var(--accent-color);
486
- color: #fff;
487
- border: none;
488
- border-radius: 4px;
489
- font-size: 11px;
490
- font-weight: 500;
491
- cursor: pointer;
492
- transition: all 0.2s;
493
- display: flex;
494
- align-items: center;
495
- gap: 4px;
496
- }
497
-
498
- .info-use-btn:hover {
499
- background: var(--accent-hover);
500
- }
501
-
502
- .info-use-btn.copied {
503
- background: var(--success-color);
504
- }
505
-
506
- .info-example-code {
507
- background: var(--bg-tertiary);
508
- border: 1px solid var(--border-color);
509
- border-radius: 6px;
510
- padding: 12px;
511
- font-family: 'SF Mono', 'Fira Code', monospace;
512
- font-size: 12px;
513
- color: var(--text-primary);
514
- overflow-x: auto;
515
- white-space: pre;
516
- margin: 0;
517
- line-height: 1.5;
518
- }
519
-
520
- .info-curl-section {
521
- margin-bottom: 16px;
522
- }
523
-
524
- .info-curl-btn {
525
- width: 100%;
526
- padding: 10px 16px;
527
- background: var(--bg-tertiary);
528
- color: var(--text-primary);
529
- border: 1px solid var(--border-color);
530
- border-radius: 6px;
531
- font-size: 12px;
532
- font-weight: 500;
533
- cursor: pointer;
534
- transition: all 0.2s;
535
- display: flex;
536
- align-items: center;
537
- justify-content: center;
538
- gap: 8px;
539
- }
540
-
541
- .info-curl-btn:hover {
542
- background: var(--accent-color);
543
- border-color: var(--accent-color);
544
- color: #fff;
545
- }
546
-
547
- .info-curl-btn.copied {
548
- background: var(--success-color);
549
- border-color: var(--success-color);
550
- color: #fff;
551
- }
552
-
553
- /* Resizer */
554
- .resizer {
555
- height: 6px;
556
- background: var(--bg-secondary);
557
- cursor: row-resize;
558
- flex-shrink: 0;
559
- display: flex;
560
- align-items: center;
561
- justify-content: center;
562
- border-top: 1px solid var(--border-color);
563
- border-bottom: 1px solid var(--border-color);
564
- }
565
-
566
- .resizer:hover { background: var(--accent-color); }
567
-
568
- .resizer-handle {
569
- width: 40px;
570
- height: 3px;
571
- background: var(--border-color);
572
- border-radius: 2px;
573
- }
574
-
575
- .resizer:hover .resizer-handle { background: #fff; }
576
-
577
- /* Response Section */
578
- .response-section {
579
- flex: 1;
580
- display: flex;
581
- flex-direction: column;
582
- min-height: 150px;
583
- overflow: hidden;
584
- }
585
-
586
- .response-header {
587
- padding: 10px 16px;
588
- background: var(--bg-secondary);
589
- border-bottom: 1px solid var(--border-color);
590
- display: flex;
591
- align-items: center;
592
- gap: 12px;
593
- flex-shrink: 0;
594
- }
595
-
596
- .response-header h3 { font-size: 13px; font-weight: 600; }
597
-
598
- .response-status {
599
- padding: 3px 8px;
600
- border-radius: 4px;
601
- font-size: 12px;
602
- font-weight: 600;
603
- display: none;
604
- }
605
-
606
- .response-status.success { display: inline; background: rgba(63,185,80,0.2); color: var(--success-color); }
607
- .response-status.error { display: inline; background: rgba(248,81,73,0.2); color: var(--error-color); }
608
-
609
- .response-meta { font-size: 12px; color: var(--text-secondary); margin-left: auto; }
610
-
611
- .response-tabs {
612
- display: flex;
613
- padding: 0 16px;
614
- background: var(--bg-secondary);
615
- border-bottom: 1px solid var(--border-color);
616
- flex-shrink: 0;
617
- }
618
-
619
- .response-tab {
620
- padding: 8px 14px;
621
- font-size: 12px;
622
- font-weight: 500;
623
- color: var(--text-secondary);
624
- cursor: pointer;
625
- border-bottom: 2px solid transparent;
626
- transition: all 0.2s;
627
- }
628
-
629
- .response-tab:hover { color: var(--text-primary); }
630
- .response-tab.active { color: var(--text-primary); border-bottom-color: var(--accent-color); }
631
-
632
- .response-tab-badge {
633
- margin-left: 4px;
634
- padding: 1px 5px;
635
- font-size: 10px;
636
- background: var(--bg-tertiary);
637
- border-radius: 8px;
638
- }
639
-
640
- .response-body { flex: 1; overflow: hidden; position: relative; display: flex; flex-direction: column; }
641
-
642
- .response-content { flex: 1; display: none; overflow: hidden; }
643
- .response-content.active { display: flex; flex-direction: column; }
644
-
645
- #responseEditor, #responseRawEditor, #responseHeadersEditor { flex: 1; min-height: 100px; }
646
-
647
- .empty-state {
648
- position: absolute;
649
- inset: 0;
650
- display: flex;
651
- flex-direction: column;
652
- align-items: center;
653
- justify-content: center;
654
- color: var(--text-muted);
655
- gap: 12px;
656
- }
657
-
658
- .empty-state .icon { font-size: 40px; opacity: 0.5; }
659
- .empty-state p { font-size: 13px; text-align: center; }
660
-
661
- .keyboard-hint {
662
- position: fixed;
663
- bottom: 16px;
664
- right: 16px;
665
- padding: 6px 10px;
666
- background: var(--bg-tertiary);
667
- border: 1px solid var(--border-color);
668
- border-radius: 5px;
669
- font-size: 11px;
670
- color: var(--text-secondary);
671
- display: flex;
672
- align-items: center;
673
- gap: 6px;
674
- }
675
-
676
- .keyboard-hint kbd {
677
- padding: 2px 5px;
678
- background: var(--bg-secondary);
679
- border: 1px solid var(--border-color);
680
- border-radius: 3px;
681
- font-family: 'SF Mono', monospace;
682
- font-size: 10px;
683
- }
684
-
685
- /* API Tabs */
686
- .api-tabs-container {
687
- display: flex;
688
- background: var(--bg-secondary);
689
- border-bottom: 1px solid var(--border-color);
690
- overflow-x: auto;
691
- flex-shrink: 0;
692
- min-height: 36px;
693
- }
694
-
695
- .api-tabs-container::-webkit-scrollbar { height: 4px; }
696
- .api-tabs-container::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 2px; }
697
-
698
- .api-tab {
699
- display: flex;
700
- align-items: center;
701
- gap: 6px;
702
- padding: 8px 12px;
703
- border-right: 1px solid var(--border-color);
704
- cursor: pointer;
705
- background: var(--bg-tertiary);
706
- transition: background 0.15s;
707
- min-width: 120px;
708
- max-width: 220px;
709
- position: relative;
710
- }
711
-
712
- .api-tab:hover { background: var(--bg-secondary); }
713
- .api-tab.active {
714
- background: var(--bg-primary);
715
- border-bottom: 2px solid var(--accent-color);
716
- margin-bottom: -1px;
717
- }
718
-
719
- .api-tab-method {
720
- font-size: 9px;
721
- font-weight: 700;
722
- padding: 2px 4px;
723
- border-radius: 3px;
724
- text-transform: uppercase;
725
- flex-shrink: 0;
726
- }
727
-
728
- .api-tab-method.get { background: var(--method-get); color: #000; }
729
- .api-tab-method.post { background: var(--method-post); color: #fff; }
730
- .api-tab-method.put { background: var(--method-put); color: #000; }
731
- .api-tab-method.patch { background: var(--method-patch); color: #fff; }
732
- .api-tab-method.delete { background: var(--method-delete); color: #fff; }
733
-
734
- .api-tab-path {
735
- font-size: 11px;
736
- font-family: 'SF Mono', 'Fira Code', monospace;
737
- white-space: nowrap;
738
- overflow: hidden;
739
- text-overflow: ellipsis;
740
- flex: 1;
741
- }
742
-
743
- .api-tab-close {
744
- padding: 2px 5px;
745
- border: none;
746
- background: transparent;
747
- color: var(--text-muted);
748
- cursor: pointer;
749
- border-radius: 3px;
750
- font-size: 14px;
751
- line-height: 1;
752
- opacity: 0;
753
- transition: all 0.15s;
754
- }
755
-
756
- .api-tab:hover .api-tab-close { opacity: 1; }
757
- .api-tab-close:hover { background: var(--error-color); color: #fff; }
758
-
759
- .api-tab-add {
760
- padding: 8px 14px;
761
- border: none;
762
- background: transparent;
763
- color: var(--text-secondary);
764
- cursor: pointer;
765
- font-size: 16px;
766
- transition: all 0.15s;
767
- flex-shrink: 0;
768
- }
769
-
770
- .api-tab-add:hover { background: var(--bg-tertiary); color: var(--text-primary); }
771
-
772
- ::-webkit-scrollbar { width: 8px; height: 8px; }
773
- ::-webkit-scrollbar-track { background: transparent; }
774
- ::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 4px; }
775
- ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
776
- </style>
777
- </head>
778
- <body>
779
- <div class="app">
780
- <aside class="sidebar" id="sidebar">
781
- <div class="sidebar-header">
782
- <span class="logo"></span>
783
- <h1>${config.title}</h1>
784
- <button class="collapse-btn" onclick="toggleSidebar()" title="Toggle sidebar">◀</button>
785
- </div>
786
- <div class="sidebar-content">
787
- <div class="search-box">
788
- <input type="text" class="search-input" placeholder="Search..." id="searchInput" onkeyup="filterEndpoints()">
789
- </div>
790
- <div class="endpoints-list" id="endpointsList"></div>
791
- </div>
792
- </aside>
793
-
794
- <main class="main">
795
- <div class="api-tabs-container" id="apiTabs"></div>
796
-
797
- <section class="request-section" id="requestSection">
798
- <div class="request-header">
799
- <div class="url-bar">
800
- <select class="method-select" id="methodSelect" onchange="updateMethodColor()">
801
- <option value="GET">GET</option>
802
- <option value="POST">POST</option>
803
- <option value="PUT">PUT</option>
804
- <option value="PATCH">PATCH</option>
805
- <option value="DELETE">DELETE</option>
806
- </select>
807
- <input type="text" class="url-input" id="urlInput" placeholder="Enter URL" value="${baseUrl}/" onchange="updateCurrentTabLabel()" onblur="updateCurrentTabLabel()">
808
- </div>
809
- <button class="send-btn" id="sendBtn" onclick="sendRequest()">
810
- <span id="sendBtnText">Send</span>
811
- <span class="spinner" id="spinner"></span>
812
- </button>
813
- </div>
814
-
815
- <div class="tabs">
816
- <div class="tab active" data-tab="body" onclick="switchTab('body')">Body</div>
817
- <div class="tab" data-tab="params" onclick="switchTab('params')">Params <span class="tab-badge" id="paramsCount">0</span></div>
818
- <div class="tab" data-tab="headers" onclick="switchTab('headers')">Headers <span class="tab-badge" id="headersCount">1</span></div>
819
- <div class="tab" data-tab="info" onclick="switchTab('info')">Info</div>
820
- </div>
821
-
822
- <div class="editor-container">
823
- <div class="editor-wrapper active" id="bodyTab">
824
- <div class="editor" id="requestEditor"></div>
825
- </div>
826
- <div class="editor-wrapper" id="paramsTab">
827
- <div class="params-editor" id="paramsEditor"></div>
828
- </div>
829
- <div class="editor-wrapper" id="headersTab">
830
- <div class="params-editor" id="headersEditor"></div>
831
- </div>
832
- <div class="editor-wrapper" id="infoTab">
833
- <div class="info-panel" id="infoPanel">
834
- <div class="info-empty">Select an endpoint to view its information</div>
835
- </div>
836
- </div>
837
- </div>
838
- </section>
839
-
840
- <div class="resizer" id="resizer">
841
- <div class="resizer-handle"></div>
842
- </div>
843
-
844
- <section class="response-section" id="responseSection">
845
- <div class="response-header">
846
- <h3>Response</h3>
847
- <span class="response-status" id="responseStatus"></span>
848
- <span class="response-meta" id="responseMeta"></span>
849
- </div>
850
- <div class="response-tabs" id="responseTabs" style="display:none;">
851
- <div class="response-tab active" data-restab="json" onclick="switchResponseTab('json')">JSON</div>
852
- <div class="response-tab" data-restab="raw" onclick="switchResponseTab('raw')">Raw</div>
853
- <div class="response-tab" data-restab="headers" onclick="switchResponseTab('headers')">Headers <span class="response-tab-badge" id="resHeadersCount">0</span></div>
854
- </div>
855
- <div class="response-body">
856
- <div class="empty-state" id="emptyResponse">
857
- <span class="icon">📡</span>
858
- <p>Click "Send" or press Ctrl+Enter</p>
859
- </div>
860
- <div class="response-content active" id="jsonContent">
861
- <div class="editor" id="responseEditor"></div>
862
- </div>
863
- <div class="response-content" id="rawContent">
864
- <div class="editor" id="responseRawEditor"></div>
865
- </div>
866
- <div class="response-content" id="headersContent">
867
- <div class="editor" id="responseHeadersEditor"></div>
868
- </div>
869
- </div>
870
- </section>
871
- </main>
872
- </div>
873
-
874
- <div class="keyboard-hint">
875
- <kbd>Ctrl</kbd>+<kbd>Enter</kbd> Send &nbsp;|&nbsp;
876
- <kbd>Ctrl</kbd>+<kbd>T</kbd> New Tab &nbsp;|&nbsp;
877
- <kbd>Ctrl</kbd>+<kbd>W</kbd> Close Tab &nbsp;|&nbsp;
878
- <kbd>Ctrl</kbd>+<kbd>Tab</kbd> Next Tab
879
- </div>
880
-
881
- <script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs/loader.js"></script>
882
- <script>
883
- let requestEditor, responseEditor, responseRawEditor, responseHeadersEditor;
884
- let routes = [];
885
- let params = [];
886
- let headers = [{ enabled: true, key: 'Content-Type', value: 'application/json' }];
887
- let lastResponseHeaders = {};
888
- let lastResponseRaw = '';
889
- let lastResponseJson = '';
890
- let currentEndpointInfo = null;
891
- const isDark = ${isDark};
892
-
893
- // Tab system
894
- let tabs = [];
895
- let activeTabId = null;
896
- const STORAGE_KEY = 'nexus_playground_tabs';
897
-
898
- function generateTabId() {
899
- return 'tab_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
900
- }
901
-
902
- function saveTabsToStorage() {
903
- try {
904
- saveCurrentTabState();
905
- const data = {
906
- tabs: tabs,
907
- activeTabId: activeTabId
908
- };
909
- localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
910
- } catch (e) {
911
- console.warn('Failed to save tabs to localStorage:', e);
912
- }
913
- }
914
-
915
- function loadTabsFromStorage() {
916
- try {
917
- const data = localStorage.getItem(STORAGE_KEY);
918
- if (data) {
919
- const parsed = JSON.parse(data);
920
- if (parsed.tabs && parsed.tabs.length > 0) {
921
- tabs = parsed.tabs;
922
- activeTabId = parsed.activeTabId;
923
- return true;
924
- }
925
- }
926
- } catch (e) {
927
- console.warn('Failed to load tabs from localStorage:', e);
928
- }
929
- return false;
930
- }
931
-
932
- function saveCurrentTabState() {
933
- if (!activeTabId) return;
934
- const tab = tabs.find(t => t.id === activeTabId);
935
- if (tab) {
936
- tab.state = {
937
- method: document.getElementById('methodSelect').value,
938
- url: document.getElementById('urlInput').value,
939
- body: requestEditor ? requestEditor.getValue() : '{}',
940
- params: JSON.parse(JSON.stringify(params)),
941
- headers: JSON.parse(JSON.stringify(headers)),
942
- response: {
943
- json: lastResponseJson,
944
- raw: lastResponseRaw,
945
- headers: lastResponseHeaders,
946
- statusText: document.getElementById('responseStatus').textContent,
947
- statusClass: document.getElementById('responseStatus').className,
948
- meta: document.getElementById('responseMeta').textContent,
949
- hasResponse: document.getElementById('responseTabs').style.display === 'flex'
950
- }
951
- };
952
- }
953
- }
954
-
955
- function loadTabState(tabId) {
956
- const tab = tabs.find(t => t.id === tabId);
957
- if (!tab) return;
958
-
959
- // If tab has saved state, restore it
960
- if (tab.state) {
961
- const state = tab.state;
962
- document.getElementById('methodSelect').value = state.method;
963
- updateMethodColorOnly();
964
- document.getElementById('urlInput').value = state.url;
965
-
966
- if (requestEditor) {
967
- requestEditor.setValue(state.body);
968
- }
969
-
970
- params = JSON.parse(JSON.stringify(state.params));
971
- headers = JSON.parse(JSON.stringify(state.headers));
972
- renderParams();
973
- renderHeaders();
974
-
975
- // Restore response
976
- if (state.response && state.response.hasResponse) {
977
- lastResponseJson = state.response.json;
978
- lastResponseRaw = state.response.raw;
979
- lastResponseHeaders = state.response.headers;
980
-
981
- document.getElementById('emptyResponse').style.display = 'none';
982
- document.getElementById('responseTabs').style.display = 'flex';
983
-
984
- if (responseEditor) responseEditor.setValue(lastResponseJson || '');
985
- if (responseRawEditor) responseRawEditor.setValue(lastResponseRaw || '');
986
-
987
- const headersText = Object.entries(lastResponseHeaders || {})
988
- .map(([k, v]) => k + ': ' + v)
989
- .join('\\n');
990
- if (responseHeadersEditor) responseHeadersEditor.setValue(headersText || 'No headers');
991
-
992
- document.getElementById('resHeadersCount').textContent = Object.keys(lastResponseHeaders || {}).length;
993
- document.getElementById('responseStatus').textContent = state.response.statusText;
994
- document.getElementById('responseStatus').className = state.response.statusClass;
995
- document.getElementById('responseMeta').textContent = state.response.meta;
996
- } else {
997
- resetResponse();
998
- }
999
- } else {
1000
- // New tab without state - initialize with tab's basic info
1001
- document.getElementById('methodSelect').value = tab.method || 'GET';
1002
- updateMethodColorOnly();
1003
- document.getElementById('urlInput').value = '${baseUrl}' + (tab.path || '/');
1004
-
1005
- if (requestEditor) {
1006
- requestEditor.setValue('{}');
1007
- }
1008
-
1009
- params = [];
1010
- headers = [{ enabled: true, key: 'Content-Type', value: 'application/json' }];
1011
- renderParams();
1012
- renderHeaders();
1013
- resetResponse();
1014
- }
1015
-
1016
- // Update info panel based on current tab
1017
- updateInfoPanelForTab(tab);
1018
- }
1019
-
1020
- function updateInfoPanelForTab(tab) {
1021
- // Find the matching endpoint from routes
1022
- const endpoint = routes.find(r => r.path === tab.path && r.method === tab.method);
1023
- currentEndpointInfo = endpoint || null;
1024
- renderInfoPanel();
1025
- }
1026
-
1027
- function resetResponse() {
1028
- document.getElementById('emptyResponse').style.display = 'flex';
1029
- document.getElementById('responseTabs').style.display = 'none';
1030
- if (responseEditor) responseEditor.setValue('');
1031
- if (responseRawEditor) responseRawEditor.setValue('');
1032
- if (responseHeadersEditor) responseHeadersEditor.setValue('');
1033
- document.getElementById('responseStatus').textContent = '';
1034
- document.getElementById('responseStatus').className = 'response-status';
1035
- document.getElementById('responseMeta').textContent = '';
1036
- lastResponseJson = '';
1037
- lastResponseRaw = '';
1038
- lastResponseHeaders = {};
1039
- }
1040
-
1041
- function createNewTab(endpoint) {
1042
- const tabId = generateTabId();
1043
- const tab = {
1044
- id: tabId,
1045
- method: endpoint ? endpoint.method : 'GET',
1046
- path: endpoint ? endpoint.path : '/',
1047
- label: endpoint ? endpoint.path : 'New Request',
1048
- state: null
1049
- };
1050
- tabs.push(tab);
1051
- return tabId;
1052
- }
1053
-
1054
- function switchToTab(tabId) {
1055
- if (activeTabId === tabId) return;
1056
-
1057
- saveCurrentTabState();
1058
- activeTabId = tabId;
1059
- loadTabState(tabId);
1060
- renderTabs();
1061
- saveTabsToStorage();
1062
-
1063
- // Highlight active endpoint in sidebar
1064
- const tab = tabs.find(t => t.id === tabId);
1065
- if (tab) {
1066
- document.querySelectorAll('.endpoint-item').forEach(el => {
1067
- if (el.dataset.path === tab.path && el.dataset.method === tab.method) {
1068
- el.classList.add('active');
1069
- } else {
1070
- el.classList.remove('active');
1071
- }
1072
- });
1073
- }
1074
- }
1075
-
1076
- function closeTab(tabId, event) {
1077
- if (event) {
1078
- event.stopPropagation();
1079
- }
1080
-
1081
- const tabIndex = tabs.findIndex(t => t.id === tabId);
1082
- if (tabIndex === -1) return;
1083
-
1084
- tabs.splice(tabIndex, 1);
1085
-
1086
- if (tabs.length === 0) {
1087
- // Create a new empty tab
1088
- const newTabId = createNewTab(null);
1089
- activeTabId = newTabId;
1090
- initializeNewTabState();
1091
- } else if (activeTabId === tabId) {
1092
- // Switch to adjacent tab
1093
- const newIndex = Math.min(tabIndex, tabs.length - 1);
1094
- activeTabId = tabs[newIndex].id;
1095
- loadTabState(activeTabId);
1096
- }
1097
-
1098
- renderTabs();
1099
- saveTabsToStorage();
1100
- }
1101
-
1102
- function initializeNewTabState() {
1103
- document.getElementById('methodSelect').value = 'GET';
1104
- updateMethodColorOnly();
1105
- document.getElementById('urlInput').value = '${baseUrl}/';
1106
- if (requestEditor) requestEditor.setValue('{}');
1107
- params = [];
1108
- headers = [{ enabled: true, key: 'Content-Type', value: 'application/json' }];
1109
- renderParams();
1110
- renderHeaders();
1111
- resetResponse();
1112
-
1113
- // Update tab info
1114
- const tab = tabs.find(t => t.id === activeTabId);
1115
- if (tab) {
1116
- tab.method = 'GET';
1117
- tab.path = '/';
1118
- tab.label = 'New Request';
1119
- }
1120
-
1121
- // Save state immediately
1122
- saveCurrentTabState();
1123
- }
1124
-
1125
- function renderTabs() {
1126
- const container = document.getElementById('apiTabs');
1127
- if (!container) return;
1128
-
1129
- let html = '';
1130
- tabs.forEach(tab => {
1131
- const isActive = tab.id === activeTabId;
1132
- const methodClass = tab.method.toLowerCase();
1133
- html += '<div class="api-tab ' + (isActive ? 'active' : '') + '" onclick="switchToTab(\\'' + tab.id + '\\')">';
1134
- html += '<span class="api-tab-method ' + methodClass + '">' + tab.method + '</span>';
1135
- html += '<span class="api-tab-path" title="' + tab.path + '">' + tab.label + '</span>';
1136
- html += '<button class="api-tab-close" onclick="closeTab(\\'' + tab.id + '\\', event)">×</button>';
1137
- html += '</div>';
1138
- });
1139
-
1140
- html += '<button class="api-tab-add" onclick="addNewTab()" title="New Tab">+</button>';
1141
- if (tabs.length > 1) {
1142
- html += '<button class="api-tab-add" onclick="clearAllTabs()" title="Clear All Tabs" style="margin-left:auto;color:var(--error-color);">🗑</button>';
1143
- }
1144
- container.innerHTML = html;
1145
- }
1146
-
1147
- function addNewTab() {
1148
- saveCurrentTabState();
1149
- const newTabId = createNewTab(null);
1150
- activeTabId = newTabId;
1151
- initializeNewTabState();
1152
- renderTabs();
1153
- saveTabsToStorage();
1154
- }
1155
-
1156
- function clearAllTabs() {
1157
- if (!confirm('Clear all tabs and start fresh?')) return;
1158
- localStorage.removeItem(STORAGE_KEY);
1159
- tabs = [];
1160
- const newTabId = createNewTab(null);
1161
- activeTabId = newTabId;
1162
- initializeNewTabState();
1163
- renderTabs();
1164
- }
1165
-
1166
- require.config({ paths: { vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.44.0/min/vs' } });
1167
-
1168
- require(['vs/editor/editor.main'], function() {
1169
- monaco.editor.defineTheme('nexus-dark', {
1170
- base: 'vs-dark',
1171
- inherit: true,
1172
- rules: [],
1173
- colors: { 'editor.background': '#0d1117' }
1174
- });
1175
-
1176
- monaco.editor.defineTheme('nexus-light', {
1177
- base: 'vs',
1178
- inherit: true,
1179
- rules: [],
1180
- colors: { 'editor.background': '#ffffff' }
1181
- });
1182
-
1183
- const theme = isDark ? 'nexus-dark' : 'nexus-light';
1184
-
1185
- requestEditor = monaco.editor.create(document.getElementById('requestEditor'), {
1186
- value: '{}',
1187
- language: 'json',
1188
- theme: theme,
1189
- minimap: { enabled: false },
1190
- fontSize: 13,
1191
- fontFamily: "'SF Mono', 'Fira Code', monospace",
1192
- lineNumbers: 'on',
1193
- scrollBeyondLastLine: false,
1194
- automaticLayout: true,
1195
- tabSize: 2,
1196
- wordWrap: 'on',
1197
- padding: { top: 12 }
1198
- });
1199
-
1200
- responseEditor = monaco.editor.create(document.getElementById('responseEditor'), {
1201
- value: '',
1202
- language: 'json',
1203
- theme: theme,
1204
- minimap: { enabled: false },
1205
- fontSize: 13,
1206
- fontFamily: "'SF Mono', 'Fira Code', monospace",
1207
- lineNumbers: 'on',
1208
- scrollBeyondLastLine: false,
1209
- automaticLayout: true,
1210
- tabSize: 2,
1211
- readOnly: true,
1212
- wordWrap: 'on',
1213
- padding: { top: 12 }
1214
- });
1215
-
1216
- responseRawEditor = monaco.editor.create(document.getElementById('responseRawEditor'), {
1217
- value: '',
1218
- language: 'plaintext',
1219
- theme: theme,
1220
- minimap: { enabled: false },
1221
- fontSize: 13,
1222
- fontFamily: "'SF Mono', 'Fira Code', monospace",
1223
- lineNumbers: 'on',
1224
- scrollBeyondLastLine: false,
1225
- automaticLayout: true,
1226
- tabSize: 2,
1227
- readOnly: true,
1228
- wordWrap: 'on',
1229
- padding: { top: 12 }
1230
- });
1231
-
1232
- responseHeadersEditor = monaco.editor.create(document.getElementById('responseHeadersEditor'), {
1233
- value: '',
1234
- language: 'plaintext',
1235
- theme: theme,
1236
- minimap: { enabled: false },
1237
- fontSize: 13,
1238
- fontFamily: "'SF Mono', 'Fira Code', monospace",
1239
- lineNumbers: 'on',
1240
- scrollBeyondLastLine: false,
1241
- automaticLayout: true,
1242
- tabSize: 2,
1243
- readOnly: true,
1244
- wordWrap: 'on',
1245
- padding: { top: 12 }
1246
- });
1247
-
1248
- requestEditor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, sendRequest);
1249
-
1250
- // Initialize tabs - load from storage or create new
1251
- const hasStoredTabs = loadTabsFromStorage();
1252
- if (hasStoredTabs && tabs.length > 0) {
1253
- renderTabs();
1254
- loadTabState(activeTabId);
1255
- } else {
1256
- const firstTabId = createNewTab(null);
1257
- activeTabId = firstTabId;
1258
- renderTabs();
1259
- }
1260
-
1261
- // Auto-save on page unload
1262
- window.addEventListener('beforeunload', saveTabsToStorage);
1263
-
1264
- loadRoutes();
1265
- renderParams();
1266
- renderHeaders();
1267
- initResizer();
1268
- });
1269
-
1270
- async function loadRoutes() {
1271
- try {
1272
- const res = await fetch('${config.path}/api/routes');
1273
- routes = await res.json();
1274
- renderEndpoints();
1275
- } catch (err) {
1276
- console.error('Failed to load routes:', err);
1277
- }
1278
- }
1279
-
1280
- function renderEndpoints() {
1281
- const container = document.getElementById('endpointsList');
1282
- const grouped = {};
1283
-
1284
- routes.forEach(route => {
1285
- const tag = route.tags?.[0] || 'General';
1286
- if (!grouped[tag]) grouped[tag] = [];
1287
- grouped[tag].push(route);
1288
- });
1289
-
1290
- let html = '';
1291
- for (const [tag, endpoints] of Object.entries(grouped)) {
1292
- html += '<div class="endpoint-group" id="group-' + tag + '">';
1293
- html += '<div class="endpoint-group-header" onclick="toggleGroup(\\'' + tag + '\\')">';
1294
- html += '<span class="arrow">▼</span>' + tag + ' <span style="opacity:0.5">(' + endpoints.length + ')</span></div>';
1295
- html += '<div class="endpoint-group-items">';
1296
-
1297
- endpoints.forEach((ep, idx) => {
1298
- const deprecated = ep.deprecated ? 'deprecated' : '';
1299
- html += '<div class="endpoint-item ' + deprecated + '" onclick="selectEndpoint(\\'' + tag + '\\',' + idx + ')" data-path="' + ep.path + '" data-method="' + ep.method + '">';
1300
- html += '<span class="method-badge ' + ep.method.toLowerCase() + '">' + ep.method + '</span>';
1301
- html += '<span class="endpoint-path" title="' + (ep.summary || ep.path) + '">' + ep.path + '</span>';
1302
- html += '</div>';
1303
- });
1304
-
1305
- html += '</div></div>';
1306
- }
1307
-
1308
- container.innerHTML = html;
1309
- }
1310
-
1311
- function toggleGroup(tag) {
1312
- document.getElementById('group-' + tag).classList.toggle('collapsed');
1313
- }
1314
-
1315
- function toggleSidebar() {
1316
- document.getElementById('sidebar').classList.toggle('collapsed');
1317
- setTimeout(() => {
1318
- requestEditor?.layout();
1319
- responseEditor?.layout();
1320
- }, 310);
1321
- }
1322
-
1323
- function selectEndpoint(tag, idx) {
1324
- const grouped = {};
1325
- routes.forEach(route => {
1326
- const t = route.tags?.[0] || 'General';
1327
- if (!grouped[t]) grouped[t] = [];
1328
- grouped[t].push(route);
1329
- });
1330
-
1331
- const endpoint = grouped[tag][idx];
1332
-
1333
- // Store current endpoint info for the Info tab
1334
- currentEndpointInfo = endpoint;
1335
- renderInfoPanel();
1336
-
1337
- // Check if tab already exists for this endpoint
1338
- const existingTab = tabs.find(t => t.path === endpoint.path && t.method === endpoint.method);
1339
-
1340
- if (existingTab) {
1341
- // Switch to existing tab
1342
- switchToTab(existingTab.id);
1343
- } else {
1344
- // Save current tab state and create new tab
1345
- saveCurrentTabState();
1346
- const newTabId = createNewTab(endpoint);
1347
- activeTabId = newTabId;
1348
-
1349
- // Set up new tab content
1350
- document.getElementById('methodSelect').value = endpoint.method;
1351
- updateMethodColorOnly();
1352
-
1353
- let url = '${baseUrl}' + endpoint.path;
1354
- document.getElementById('urlInput').value = url;
1355
-
1356
- if (endpoint.schema?.body) {
1357
- requestEditor.setValue(JSON.stringify(endpoint.schema.body, null, 2));
1358
- } else {
1359
- requestEditor.setValue('{}');
1360
- }
1361
-
1362
- params = [];
1363
- if (endpoint.schema?.query) {
1364
- endpoint.schema.query.forEach(q => {
1365
- params.push({ enabled: !q.optional, key: q.name, value: '' });
1366
- });
1367
- }
1368
- headers = [{ enabled: true, key: 'Content-Type', value: 'application/json' }];
1369
- renderParams();
1370
- renderHeaders();
1371
- resetResponse();
1372
-
1373
- // Immediately save the new tab state
1374
- saveCurrentTabState();
1375
-
1376
- renderTabs();
1377
- saveTabsToStorage();
1378
- }
1379
-
1380
- // Highlight in sidebar
1381
- document.querySelectorAll('.endpoint-item').forEach(el => {
1382
- if (el.dataset.path === endpoint.path && el.dataset.method === endpoint.method) {
1383
- el.classList.add('active');
1384
- } else {
1385
- el.classList.remove('active');
1386
- }
1387
- });
1388
- }
1389
-
1390
- function updateMethodColorOnly() {
1391
- const select = document.getElementById('methodSelect');
1392
- const method = select.value.toLowerCase();
1393
- const colors = { get: '#3fb950', post: '#2f81f7', put: '#d29922', patch: '#a371f7', delete: '#f85149' };
1394
- select.style.background = colors[method];
1395
- select.style.color = ['get', 'put'].includes(method) ? '#000' : '#fff';
1396
- }
1397
-
1398
- function updateMethodColor() {
1399
- updateMethodColorOnly();
1400
- updateCurrentTabLabel();
1401
- }
1402
-
1403
- function updateCurrentTabLabel() {
1404
- if (!activeTabId) return;
1405
- const tab = tabs.find(t => t.id === activeTabId);
1406
- if (tab) {
1407
- const url = document.getElementById('urlInput').value;
1408
- const method = document.getElementById('methodSelect').value;
1409
- // Extract path from URL
1410
- try {
1411
- const urlObj = new URL(url);
1412
- tab.path = urlObj.pathname;
1413
- } catch (e) {
1414
- tab.path = url.replace(/^https?:\\/\\/[^\\/]+/, '') || '/';
1415
- }
1416
- tab.method = method;
1417
- tab.label = tab.path;
1418
- renderTabs();
1419
- }
1420
- }
1421
-
1422
- function renderInfoPanel() {
1423
- const panel = document.getElementById('infoPanel');
1424
- if (!currentEndpointInfo) {
1425
- panel.innerHTML = '<div class="info-empty">Select an endpoint to view its information</div>';
1426
- return;
1427
- }
1428
-
1429
- const ep = currentEndpointInfo;
1430
- let html = '';
1431
-
1432
- // Method and Path header
1433
- html += '<div class="info-method-path">';
1434
- html += '<span class="method-badge ' + ep.method.toLowerCase() + '">' + ep.method + '</span>';
1435
- html += '<span class="path">' + ep.path + '</span>';
1436
- html += '</div>';
1437
-
1438
- // Copy as CURL button
1439
- html += '<div class="info-curl-section">';
1440
- html += '<button class="info-curl-btn" onclick="copyAsCurl()" id="curlCopyBtn">';
1441
- html += '<span>📋</span> Copy as CURL';
1442
- html += '</button>';
1443
- html += '</div>';
1444
-
1445
- // Deprecated warning
1446
- if (ep.deprecated) {
1447
- html += '<div class="info-section">';
1448
- html += '<div class="info-deprecated">⚠️ This endpoint is deprecated</div>';
1449
- html += '</div>';
1450
- }
1451
-
1452
- // Summary
1453
- if (ep.summary) {
1454
- html += '<div class="info-section">';
1455
- html += '<div class="info-label">Summary</div>';
1456
- html += '<div class="info-value">' + escapeHtml(ep.summary) + '</div>';
1457
- html += '</div>';
1458
- }
1459
-
1460
- // Description
1461
- if (ep.description) {
1462
- html += '<div class="info-section">';
1463
- html += '<div class="info-label">Description</div>';
1464
- html += '<div class="info-value">' + escapeHtml(ep.description) + '</div>';
1465
- html += '</div>';
1466
- }
1467
-
1468
- // Tags
1469
- if (ep.tags && ep.tags.length > 0) {
1470
- html += '<div class="info-section">';
1471
- html += '<div class="info-label">Tags</div>';
1472
- html += '<div class="info-tags">';
1473
- ep.tags.forEach(tag => {
1474
- html += '<span class="info-tag">' + escapeHtml(tag) + '</span>';
1475
- });
1476
- html += '</div>';
1477
- html += '</div>';
1478
- }
1479
-
1480
- // Responses
1481
- if (ep.responses && Object.keys(ep.responses).length > 0) {
1482
- html += '<div class="info-section">';
1483
- html += '<div class="info-label">Responses</div>';
1484
- html += '<div class="info-responses">';
1485
- Object.entries(ep.responses).forEach(([code, desc]) => {
1486
- const codeNum = parseInt(code);
1487
- let codeClass = 'success';
1488
- if (codeNum >= 300 && codeNum < 400) codeClass = 'redirect';
1489
- else if (codeNum >= 400 && codeNum < 500) codeClass = 'client-error';
1490
- else if (codeNum >= 500) codeClass = 'server-error';
1491
-
1492
- html += '<div class="info-response-item">';
1493
- html += '<span class="info-response-code ' + codeClass + '">' + code + '</span>';
1494
- html += '<span class="info-response-desc">' + escapeHtml(desc) + '</span>';
1495
- html += '</div>';
1496
- });
1497
- html += '</div>';
1498
- html += '</div>';
1499
- }
1500
-
1501
- // Example
1502
- if (ep.example) {
1503
- html += '<div class="info-section">';
1504
- html += '<div class="info-label-row">';
1505
- html += '<div class="info-label">Example Body</div>';
1506
- html += '<button class="info-use-btn" onclick="useExampleAsBody()">📋 Use as Body</button>';
1507
- html += '</div>';
1508
- html += '<pre class="info-example-code">' + escapeHtml(ep.example) + '</pre>';
1509
- html += '</div>';
1510
- }
1511
-
1512
- // Schema info
1513
- if (ep.schema) {
1514
- if (ep.schema.params && ep.schema.params.length > 0) {
1515
- html += '<div class="info-section">';
1516
- html += '<div class="info-label">Path Parameters</div>';
1517
- html += '<div class="info-responses">';
1518
- ep.schema.params.forEach(p => {
1519
- html += '<div class="info-response-item">';
1520
- html += '<span class="info-response-code success">' + escapeHtml(p.name) + '</span>';
1521
- html += '<span class="info-response-desc">' + escapeHtml(p.type || 'string') + (p.optional ? ' (optional)' : ' (required)') + '</span>';
1522
- html += '</div>';
1523
- });
1524
- html += '</div>';
1525
- html += '</div>';
1526
- }
1527
-
1528
- if (ep.schema.query && ep.schema.query.length > 0) {
1529
- html += '<div class="info-section">';
1530
- html += '<div class="info-label">Query Parameters</div>';
1531
- html += '<div class="info-responses">';
1532
- ep.schema.query.forEach(q => {
1533
- html += '<div class="info-response-item">';
1534
- html += '<span class="info-response-code redirect">' + escapeHtml(q.name) + '</span>';
1535
- html += '<span class="info-response-desc">' + escapeHtml(q.type || 'string') + (q.optional ? ' (optional)' : ' (required)') + '</span>';
1536
- html += '</div>';
1537
- });
1538
- html += '</div>';
1539
- html += '</div>';
1540
- }
1541
- }
1542
-
1543
- if (html === '') {
1544
- html = '<div class="info-empty">No additional information available for this endpoint</div>';
1545
- }
1546
-
1547
- panel.innerHTML = html;
1548
- }
1549
-
1550
- function escapeHtml(text) {
1551
- if (!text) return '';
1552
- const div = document.createElement('div');
1553
- div.textContent = text;
1554
- return div.innerHTML;
1555
- }
1556
-
1557
- function useExampleAsBody() {
1558
- if (!currentEndpointInfo || !currentEndpointInfo.example) return;
1559
-
1560
- // Get the example and try to format it as pretty JSON
1561
- let example = currentEndpointInfo.example;
1562
- try {
1563
- // Parse and re-stringify for proper formatting
1564
- const parsed = JSON.parse(example);
1565
- example = JSON.stringify(parsed, null, 2);
1566
- } catch (e) {
1567
- // If not valid JSON, use as-is
1568
- }
1569
-
1570
- // Set the example to the request body editor
1571
- if (requestEditor) {
1572
- requestEditor.setValue(example);
1573
- }
1574
-
1575
- // Switch to Body tab
1576
- switchTab('body');
1577
-
1578
- // Visual feedback on button
1579
- const btn = document.querySelector('.info-use-btn');
1580
- if (btn) {
1581
- const originalText = btn.innerHTML;
1582
- btn.innerHTML = '\u2713 Copied to Body';
1583
- btn.classList.add('copied');
1584
- setTimeout(() => {
1585
- btn.innerHTML = originalText;
1586
- btn.classList.remove('copied');
1587
- }, 1500);
1588
- }
1589
-
1590
- // Save tab state
1591
- saveCurrentTabState();
1592
- }
1593
-
1594
- function copyAsCurl() {
1595
- const method = document.getElementById('methodSelect').value;
1596
- const url = document.getElementById('urlInput').value;
1597
- const body = requestEditor ? requestEditor.getValue() : '{}';
1598
-
1599
- // Build CURL command
1600
- let curl = 'curl';
1601
-
1602
- // Add method
1603
- if (method !== 'GET') {
1604
- curl += ' -X ' + method;
1605
- }
1606
-
1607
- // Add URL (with query params)
1608
- let fullUrl = url;
1609
- const enabledParams = params.filter(p => p.enabled && p.key);
1610
- if (enabledParams.length > 0) {
1611
- const queryString = enabledParams
1612
- .map(p => encodeURIComponent(p.key) + '=' + encodeURIComponent(p.value))
1613
- .join('&');
1614
- fullUrl += (fullUrl.includes('?') ? '&' : '?') + queryString;
1615
- }
1616
- curl += " '" + fullUrl + "'";
1617
-
1618
- // Add headers
1619
- headers.filter(h => h.enabled && h.key).forEach(h => {
1620
- curl += " -H '" + h.key + ": " + h.value + "'";
1621
- });
1622
-
1623
- // Add body for non-GET requests
1624
- if (method !== 'GET' && method !== 'DELETE') {
1625
- try {
1626
- // Check if body is valid JSON and not empty
1627
- const parsedBody = JSON.parse(body);
1628
- if (Object.keys(parsedBody).length > 0) {
1629
- // Escape single quotes in body for shell
1630
- const escapedBody = body.replace(/'/g, "'\\''");
1631
- curl += " -d '" + escapedBody + "'";
1632
- }
1633
- } catch (e) {
1634
- // If not valid JSON but has content, still include it
1635
- if (body && body.trim() !== '' && body.trim() !== '{}') {
1636
- const escapedBody = body.replace(/'/g, "'\\''");
1637
- curl += " -d '" + escapedBody + "'";
1638
- }
1639
- }
1640
- }
1641
-
1642
- // Copy to clipboard
1643
- navigator.clipboard.writeText(curl).then(() => {
1644
- // Visual feedback
1645
- const btn = document.getElementById('curlCopyBtn');
1646
- if (btn) {
1647
- const originalText = btn.innerHTML;
1648
- btn.innerHTML = '<span>✓</span> Copied!';
1649
- btn.classList.add('copied');
1650
- setTimeout(() => {
1651
- btn.innerHTML = originalText;
1652
- btn.classList.remove('copied');
1653
- }, 2000);
1654
- }
1655
- }).catch(err => {
1656
- console.error('Failed to copy CURL:', err);
1657
- alert('Failed to copy CURL to clipboard');
1658
- });
1659
- }
1660
-
1661
- function switchTab(tab) {
1662
- document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
1663
- document.querySelectorAll('.editor-wrapper').forEach(w => w.classList.remove('active'));
1664
- document.querySelector('.tab[data-tab="' + tab + '"]').classList.add('active');
1665
- document.getElementById(tab + 'Tab').classList.add('active');
1666
- requestEditor?.layout();
1667
- }
1668
-
1669
- function switchResponseTab(tab) {
1670
- document.querySelectorAll('.response-tab').forEach(t => t.classList.remove('active'));
1671
- document.querySelectorAll('.response-content').forEach(c => c.classList.remove('active'));
1672
- document.querySelector('.response-tab[data-restab="' + tab + '"]').classList.add('active');
1673
- document.getElementById(tab + 'Content').classList.add('active');
1674
-
1675
- // Force layout update for the active editor
1676
- setTimeout(() => {
1677
- if (tab === 'json') responseEditor?.layout();
1678
- else if (tab === 'raw') responseRawEditor?.layout();
1679
- else if (tab === 'headers') responseHeadersEditor?.layout();
1680
- }, 10);
1681
- }
1682
-
1683
- function renderParams() {
1684
- const container = document.getElementById('paramsEditor');
1685
- let html = '';
1686
- params.forEach((p, i) => {
1687
- html += '<div class="param-row">';
1688
- html += '<input type="checkbox" class="param-checkbox" ' + (p.enabled ? 'checked' : '') + ' onchange="params[' + i + '].enabled=this.checked;updateParamsCount()">';
1689
- html += '<input type="text" class="param-input key" placeholder="Key" value="' + p.key + '" onchange="params[' + i + '].key=this.value">';
1690
- html += '<input type="text" class="param-input" placeholder="Value" value="' + p.value + '" onchange="params[' + i + '].value=this.value">';
1691
- html += '<button class="param-delete" onclick="params.splice(' + i + ',1);renderParams()">✕</button>';
1692
- html += '</div>';
1693
- });
1694
- html += '<button class="add-param-btn" onclick="params.push({enabled:true,key:\\'\\',value:\\'\\'});renderParams()">+ Add</button>';
1695
- container.innerHTML = html;
1696
- updateParamsCount();
1697
- }
1698
-
1699
- function updateParamsCount() {
1700
- document.getElementById('paramsCount').textContent = params.filter(p => p.enabled && p.key).length;
1701
- }
1702
-
1703
- function renderHeaders() {
1704
- const container = document.getElementById('headersEditor');
1705
- let html = '';
1706
- headers.forEach((h, i) => {
1707
- html += '<div class="param-row">';
1708
- html += '<input type="checkbox" class="param-checkbox" ' + (h.enabled ? 'checked' : '') + ' onchange="headers[' + i + '].enabled=this.checked;updateHeadersCount()">';
1709
- html += '<input type="text" class="param-input key" placeholder="Key" value="' + h.key + '" onchange="headers[' + i + '].key=this.value">';
1710
- html += '<input type="text" class="param-input" placeholder="Value" value="' + h.value + '" onchange="headers[' + i + '].value=this.value">';
1711
- html += '<button class="param-delete" onclick="headers.splice(' + i + ',1);renderHeaders()">✕</button>';
1712
- html += '</div>';
1713
- });
1714
- html += '<button class="add-param-btn" onclick="headers.push({enabled:true,key:\\'\\',value:\\'\\'});renderHeaders()">+ Add</button>';
1715
- container.innerHTML = html;
1716
- updateHeadersCount();
1717
- }
1718
-
1719
- function updateHeadersCount() {
1720
- document.getElementById('headersCount').textContent = headers.filter(h => h.enabled && h.key).length;
1721
- }
1722
-
1723
- async function sendRequest() {
1724
- const method = document.getElementById('methodSelect').value;
1725
- let url = document.getElementById('urlInput').value;
1726
-
1727
- const queryParams = params.filter(p => p.enabled && p.key);
1728
- if (queryParams.length > 0) {
1729
- const sp = new URLSearchParams();
1730
- queryParams.forEach(p => sp.append(p.key, p.value));
1731
- url += (url.includes('?') ? '&' : '?') + sp.toString();
1732
- }
1733
-
1734
- const reqHeaders = {};
1735
- headers.filter(h => h.enabled && h.key).forEach(h => reqHeaders[h.key] = h.value);
1736
-
1737
- let body = undefined;
1738
- if (['POST', 'PUT', 'PATCH'].includes(method)) {
1739
- const bodyContent = requestEditor.getValue().trim();
1740
- if (bodyContent && bodyContent !== '{}') {
1741
- body = bodyContent;
1742
- }
1743
- }
1744
-
1745
- const sendBtn = document.getElementById('sendBtn');
1746
- const sendBtnText = document.getElementById('sendBtnText');
1747
- const spinner = document.getElementById('spinner');
1748
- const emptyState = document.getElementById('emptyResponse');
1749
- const responseTabs = document.getElementById('responseTabs');
1750
-
1751
- sendBtn.disabled = true;
1752
- sendBtnText.textContent = 'Sending...';
1753
- spinner.style.display = 'block';
1754
-
1755
- const startTime = performance.now();
1756
-
1757
- try {
1758
- const fetchOptions = { method, headers: reqHeaders };
1759
- if (body) fetchOptions.body = body;
1760
-
1761
- console.log('Sending request:', { url, method, headers: reqHeaders, body });
1762
-
1763
- const res = await fetch(url, fetchOptions);
1764
- const endTime = performance.now();
1765
- const duration = Math.round(endTime - startTime);
1766
-
1767
- // Store raw response
1768
- lastResponseRaw = await res.text();
1769
- let responseSize = new Blob([lastResponseRaw]).size;
1770
-
1771
- console.log('Response received:', { status: res.status, text: lastResponseRaw });
1772
-
1773
- // Store response headers
1774
- lastResponseHeaders = {};
1775
- res.headers.forEach((value, key) => {
1776
- lastResponseHeaders[key] = value;
1777
- });
1778
-
1779
- // Format headers for display
1780
- const headersText = Object.entries(lastResponseHeaders)
1781
- .map(([k, v]) => k + ': ' + v)
1782
- .join('\\n');
1783
-
1784
- // Update headers count badge
1785
- document.getElementById('resHeadersCount').textContent = Object.keys(lastResponseHeaders).length;
1786
-
1787
- // Try parse JSON for pretty display
1788
- lastResponseJson = lastResponseRaw;
1789
- try {
1790
- const json = JSON.parse(lastResponseRaw);
1791
- lastResponseJson = JSON.stringify(json, null, 2);
1792
- } catch (e) {
1793
- // Not JSON, keep as is
1794
- }
1795
-
1796
- // Show response tabs and content
1797
- emptyState.style.display = 'none';
1798
- responseTabs.style.display = 'flex';
1799
-
1800
- // Set all editors content
1801
- responseEditor.setValue(lastResponseJson);
1802
- responseRawEditor.setValue(lastResponseRaw);
1803
- responseHeadersEditor.setValue(headersText || 'No headers received');
1804
-
1805
- // Force layout
1806
- setTimeout(() => {
1807
- responseEditor.layout();
1808
- responseRawEditor.layout();
1809
- responseHeadersEditor.layout();
1810
- }, 50);
1811
-
1812
- const statusEl = document.getElementById('responseStatus');
1813
- statusEl.textContent = res.status + ' ' + res.statusText;
1814
- statusEl.className = 'response-status ' + (res.ok ? 'success' : 'error');
1815
-
1816
- document.getElementById('responseMeta').textContent = duration + 'ms • ' + formatBytes(responseSize);
1817
-
1818
- } catch (err) {
1819
- console.error('Request failed:', err);
1820
-
1821
- const errorResponse = {
1822
- error: err.message,
1823
- hint: 'Make sure the server is running and the URL is correct'
1824
- };
1825
-
1826
- lastResponseJson = JSON.stringify(errorResponse, null, 2);
1827
- lastResponseRaw = err.message;
1828
- lastResponseHeaders = {};
1829
-
1830
- // Show response tabs and content
1831
- emptyState.style.display = 'none';
1832
- responseTabs.style.display = 'flex';
1833
-
1834
- responseEditor.setValue(lastResponseJson);
1835
- responseRawEditor.setValue(lastResponseRaw);
1836
- responseHeadersEditor.setValue('No headers (request failed)');
1837
- document.getElementById('resHeadersCount').textContent = '0';
1838
-
1839
- setTimeout(() => {
1840
- responseEditor.layout();
1841
- responseRawEditor.layout();
1842
- responseHeadersEditor.layout();
1843
- }, 50);
1844
-
1845
- const statusEl = document.getElementById('responseStatus');
1846
- statusEl.textContent = 'Error';
1847
- statusEl.className = 'response-status error';
1848
- document.getElementById('responseMeta').textContent = '';
1849
- } finally {
1850
- sendBtn.disabled = false;
1851
- sendBtnText.textContent = 'Send';
1852
- spinner.style.display = 'none';
1853
- saveTabsToStorage();
1854
- }
1855
- }
1856
-
1857
- function formatBytes(bytes) {
1858
- if (bytes < 1024) return bytes + ' B';
1859
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
1860
- return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
1861
- }
1862
-
1863
- function filterEndpoints() {
1864
- const query = document.getElementById('searchInput').value.toLowerCase();
1865
- document.querySelectorAll('.endpoint-item').forEach(el => {
1866
- const match = el.dataset.path.toLowerCase().includes(query) || el.dataset.method.toLowerCase().includes(query);
1867
- el.style.display = match ? 'flex' : 'none';
1868
- });
1869
- }
1870
-
1871
- function initResizer() {
1872
- const resizer = document.getElementById('resizer');
1873
- const requestSection = document.getElementById('requestSection');
1874
- const main = document.querySelector('.main');
1875
- let isResizing = false;
1876
- let startY = 0;
1877
- let startHeight = 0;
1878
-
1879
- resizer.addEventListener('mousedown', (e) => {
1880
- isResizing = true;
1881
- startY = e.clientY;
1882
- startHeight = requestSection.offsetHeight;
1883
- document.body.style.cursor = 'row-resize';
1884
- document.body.style.userSelect = 'none';
1885
- });
1886
-
1887
- document.addEventListener('mousemove', (e) => {
1888
- if (!isResizing) return;
1889
-
1890
- const deltaY = e.clientY - startY;
1891
- const newHeight = startHeight + deltaY;
1892
- const mainHeight = main.offsetHeight;
1893
- const minHeight = 150;
1894
- const maxHeight = mainHeight - 150;
1895
-
1896
- if (newHeight >= minHeight && newHeight <= maxHeight) {
1897
- requestSection.style.height = newHeight + 'px';
1898
- requestEditor?.layout();
1899
- responseEditor?.layout();
1900
- }
1901
- });
1902
-
1903
- document.addEventListener('mouseup', () => {
1904
- if (isResizing) {
1905
- isResizing = false;
1906
- document.body.style.cursor = '';
1907
- document.body.style.userSelect = '';
1908
- requestEditor?.layout();
1909
- responseEditor?.layout();
1910
- }
1911
- });
1912
- }
1913
-
1914
- document.addEventListener('keydown', (e) => {
1915
- // Ctrl+Enter - Send request
1916
- if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
1917
- e.preventDefault();
1918
- sendRequest();
1919
- }
1920
- // Ctrl+W - Close current tab
1921
- if ((e.ctrlKey || e.metaKey) && e.key === 'w') {
1922
- e.preventDefault();
1923
- if (activeTabId) {
1924
- closeTab(activeTabId, null);
1925
- }
1926
- }
1927
- // Ctrl+Tab - Next tab
1928
- if ((e.ctrlKey || e.metaKey) && e.key === 'Tab' && !e.shiftKey) {
1929
- e.preventDefault();
1930
- if (tabs.length > 1) {
1931
- const currentIndex = tabs.findIndex(t => t.id === activeTabId);
1932
- const nextIndex = (currentIndex + 1) % tabs.length;
1933
- switchToTab(tabs[nextIndex].id);
1934
- }
1935
- }
1936
- // Ctrl+Shift+Tab - Previous tab
1937
- if ((e.ctrlKey || e.metaKey) && e.key === 'Tab' && e.shiftKey) {
1938
- e.preventDefault();
1939
- if (tabs.length > 1) {
1940
- const currentIndex = tabs.findIndex(t => t.id === activeTabId);
1941
- const prevIndex = (currentIndex - 1 + tabs.length) % tabs.length;
1942
- switchToTab(tabs[prevIndex].id);
1943
- }
1944
- }
1945
- // Ctrl+T - New tab
1946
- if ((e.ctrlKey || e.metaKey) && e.key === 't') {
1947
- e.preventDefault();
1948
- addNewTab();
1949
- }
1950
- });
1951
-
1952
- updateMethodColor();
1953
- </script>
1954
- </body>
1955
- </html>`;
1956
- }