@engjts/nexus 0.1.8 → 0.1.10

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 (221) hide show
  1. package/dist/advanced/playground/playground.js.map +1 -1
  2. package/dist/advanced/static/generateDirectoryListing.d.ts +1 -1
  3. package/dist/advanced/static/generateDirectoryListing.d.ts.map +1 -1
  4. package/dist/advanced/static/generateDirectoryListing.js +12 -6
  5. package/dist/advanced/static/generateDirectoryListing.js.map +1 -1
  6. package/dist/advanced/static/index.d.ts +2 -0
  7. package/dist/advanced/static/index.d.ts.map +1 -1
  8. package/dist/advanced/static/index.js +4 -1
  9. package/dist/advanced/static/index.js.map +1 -1
  10. package/dist/advanced/static/serveStatic.d.ts.map +1 -1
  11. package/dist/advanced/static/serveStatic.js +7 -1
  12. package/dist/advanced/static/serveStatic.js.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +3 -1
  16. package/dist/index.js.map +1 -1
  17. package/package.json +1 -1
  18. package/BENCHMARK_REPORT.md +0 -343
  19. package/documentation/01-getting-started.md +0 -240
  20. package/documentation/02-context.md +0 -335
  21. package/documentation/03-routing.md +0 -397
  22. package/documentation/04-middleware.md +0 -483
  23. package/documentation/05-validation.md +0 -514
  24. package/documentation/06-error-handling.md +0 -465
  25. package/documentation/07-performance.md +0 -364
  26. package/documentation/08-adapters.md +0 -470
  27. package/documentation/09-api-reference.md +0 -548
  28. package/documentation/10-examples.md +0 -582
  29. package/documentation/11-deployment.md +0 -477
  30. package/documentation/12-sentry.md +0 -620
  31. package/documentation/13-sentry-data-storage.md +0 -996
  32. package/documentation/14-sentry-data-reference.md +0 -457
  33. package/documentation/15-sentry-summary.md +0 -409
  34. package/documentation/16-alerts-system.md +0 -745
  35. package/documentation/17-alert-adapters.md +0 -696
  36. package/documentation/18-alerts-implementation-summary.md +0 -385
  37. package/documentation/19-class-based-routing.md +0 -840
  38. package/documentation/20-websocket-realtime.md +0 -813
  39. package/documentation/21-cache-system.md +0 -510
  40. package/documentation/22-job-queue.md +0 -772
  41. package/documentation/23-sentry-plugin.md +0 -551
  42. package/documentation/24-testing-utilities.md +0 -1287
  43. package/documentation/25-api-versioning.md +0 -533
  44. package/documentation/26-context-store.md +0 -607
  45. package/documentation/27-dependency-injection.md +0 -329
  46. package/documentation/28-lifecycle-hooks.md +0 -521
  47. package/documentation/29-package-structure.md +0 -196
  48. package/documentation/30-plugin-system.md +0 -414
  49. package/documentation/31-jwt-authentication.md +0 -597
  50. package/documentation/32-cli.md +0 -268
  51. package/documentation/ALERTS-COMPLETE-SUMMARY.md +0 -429
  52. package/documentation/ALERTS-INDEX.md +0 -330
  53. package/documentation/ALERTS-QUICK-REFERENCE.md +0 -286
  54. package/documentation/README.md +0 -178
  55. package/documentation/index.html +0 -34
  56. package/modern_framework_paper.md +0 -1870
  57. package/public/css/style.css +0 -87
  58. package/public/index.html +0 -34
  59. package/public/js/app.js +0 -27
  60. package/src/advanced/cache/InMemoryCacheStore.ts +0 -68
  61. package/src/advanced/cache/MultiTierCache.ts +0 -194
  62. package/src/advanced/cache/RedisCacheStore.ts +0 -341
  63. package/src/advanced/cache/index.ts +0 -5
  64. package/src/advanced/cache/types.ts +0 -40
  65. package/src/advanced/graphql/SimpleDataLoader.ts +0 -42
  66. package/src/advanced/graphql/index.ts +0 -22
  67. package/src/advanced/graphql/server.ts +0 -252
  68. package/src/advanced/graphql/types.ts +0 -42
  69. package/src/advanced/jobs/InMemoryQueueStore.ts +0 -68
  70. package/src/advanced/jobs/JobQueue.ts +0 -556
  71. package/src/advanced/jobs/RedisQueueStore.ts +0 -367
  72. package/src/advanced/jobs/index.ts +0 -5
  73. package/src/advanced/jobs/types.ts +0 -70
  74. package/src/advanced/observability/APMManager.ts +0 -163
  75. package/src/advanced/observability/AlertManager.ts +0 -109
  76. package/src/advanced/observability/MetricRegistry.ts +0 -151
  77. package/src/advanced/observability/ObservabilityCenter.ts +0 -304
  78. package/src/advanced/observability/StructuredLogger.ts +0 -154
  79. package/src/advanced/observability/TracingManager.ts +0 -117
  80. package/src/advanced/observability/adapters.ts +0 -304
  81. package/src/advanced/observability/createObservabilityMiddleware.ts +0 -63
  82. package/src/advanced/observability/index.ts +0 -11
  83. package/src/advanced/observability/types.ts +0 -174
  84. package/src/advanced/playground/extractPathParams.ts +0 -6
  85. package/src/advanced/playground/generateFieldExample.ts +0 -31
  86. package/src/advanced/playground/generatePlaygroundHTML.ts +0 -1956
  87. package/src/advanced/playground/generateSummary.ts +0 -19
  88. package/src/advanced/playground/getTagFromPath.ts +0 -9
  89. package/src/advanced/playground/index.ts +0 -8
  90. package/src/advanced/playground/playground.ts +0 -250
  91. package/src/advanced/playground/types.ts +0 -49
  92. package/src/advanced/playground/zodToExample.ts +0 -16
  93. package/src/advanced/playground/zodToParams.ts +0 -15
  94. package/src/advanced/postman/buildAuth.ts +0 -31
  95. package/src/advanced/postman/buildBody.ts +0 -15
  96. package/src/advanced/postman/buildQueryParams.ts +0 -27
  97. package/src/advanced/postman/buildRequestItem.ts +0 -36
  98. package/src/advanced/postman/buildResponses.ts +0 -11
  99. package/src/advanced/postman/buildUrl.ts +0 -33
  100. package/src/advanced/postman/capitalize.ts +0 -4
  101. package/src/advanced/postman/generateCollection.ts +0 -59
  102. package/src/advanced/postman/generateEnvironment.ts +0 -34
  103. package/src/advanced/postman/generateExampleFromZod.ts +0 -21
  104. package/src/advanced/postman/generateFieldExample.ts +0 -45
  105. package/src/advanced/postman/generateName.ts +0 -20
  106. package/src/advanced/postman/generateUUID.ts +0 -11
  107. package/src/advanced/postman/getTagFromPath.ts +0 -10
  108. package/src/advanced/postman/index.ts +0 -28
  109. package/src/advanced/postman/postman.ts +0 -156
  110. package/src/advanced/postman/slugify.ts +0 -7
  111. package/src/advanced/postman/types.ts +0 -140
  112. package/src/advanced/realtime/index.ts +0 -18
  113. package/src/advanced/realtime/websocket.ts +0 -231
  114. package/src/advanced/sentry/index.ts +0 -1236
  115. package/src/advanced/sentry/types.ts +0 -355
  116. package/src/advanced/static/generateDirectoryListing.ts +0 -47
  117. package/src/advanced/static/generateETag.ts +0 -7
  118. package/src/advanced/static/getMimeType.ts +0 -9
  119. package/src/advanced/static/index.ts +0 -32
  120. package/src/advanced/static/isSafePath.ts +0 -13
  121. package/src/advanced/static/publicDir.ts +0 -21
  122. package/src/advanced/static/serveStatic.ts +0 -225
  123. package/src/advanced/static/spa.ts +0 -24
  124. package/src/advanced/static/types.ts +0 -159
  125. package/src/advanced/swagger/SwaggerGenerator.ts +0 -66
  126. package/src/advanced/swagger/buildOperation.ts +0 -61
  127. package/src/advanced/swagger/buildParameters.ts +0 -61
  128. package/src/advanced/swagger/buildRequestBody.ts +0 -21
  129. package/src/advanced/swagger/buildResponses.ts +0 -54
  130. package/src/advanced/swagger/capitalize.ts +0 -5
  131. package/src/advanced/swagger/convertPath.ts +0 -9
  132. package/src/advanced/swagger/createSwagger.ts +0 -12
  133. package/src/advanced/swagger/generateOperationId.ts +0 -21
  134. package/src/advanced/swagger/generateSpec.ts +0 -105
  135. package/src/advanced/swagger/generateSummary.ts +0 -24
  136. package/src/advanced/swagger/generateSwaggerUI.ts +0 -70
  137. package/src/advanced/swagger/generateThemeCss.ts +0 -53
  138. package/src/advanced/swagger/index.ts +0 -25
  139. package/src/advanced/swagger/swagger.ts +0 -237
  140. package/src/advanced/swagger/types.ts +0 -206
  141. package/src/advanced/swagger/zodFieldToOpenAPI.ts +0 -94
  142. package/src/advanced/swagger/zodSchemaToOpenAPI.ts +0 -50
  143. package/src/advanced/swagger/zodToOpenAPI.ts +0 -22
  144. package/src/advanced/testing/factory.ts +0 -509
  145. package/src/advanced/testing/harness.ts +0 -612
  146. package/src/advanced/testing/index.ts +0 -430
  147. package/src/advanced/testing/load-test.ts +0 -618
  148. package/src/advanced/testing/mock-server.ts +0 -498
  149. package/src/advanced/testing/mock.ts +0 -670
  150. package/src/cli/bin.ts +0 -9
  151. package/src/cli/cli.ts +0 -158
  152. package/src/cli/commands/add.ts +0 -178
  153. package/src/cli/commands/build.ts +0 -73
  154. package/src/cli/commands/create.ts +0 -166
  155. package/src/cli/commands/dev.ts +0 -85
  156. package/src/cli/commands/generate.ts +0 -99
  157. package/src/cli/commands/help.ts +0 -95
  158. package/src/cli/commands/init.ts +0 -91
  159. package/src/cli/commands/version.ts +0 -38
  160. package/src/cli/index.ts +0 -6
  161. package/src/cli/templates/generators.ts +0 -359
  162. package/src/cli/templates/index.ts +0 -680
  163. package/src/cli/utils/exec.ts +0 -52
  164. package/src/cli/utils/file-system.ts +0 -78
  165. package/src/cli/utils/logger.ts +0 -111
  166. package/src/core/adapter.ts +0 -88
  167. package/src/core/application.ts +0 -1453
  168. package/src/core/context-pool.ts +0 -79
  169. package/src/core/context.ts +0 -856
  170. package/src/core/index.ts +0 -94
  171. package/src/core/middleware.ts +0 -272
  172. package/src/core/performance/buffer-pool.ts +0 -108
  173. package/src/core/performance/middleware-optimizer.ts +0 -162
  174. package/src/core/plugin/PluginManager.ts +0 -435
  175. package/src/core/plugin/builder.ts +0 -358
  176. package/src/core/plugin/index.ts +0 -50
  177. package/src/core/plugin/types.ts +0 -214
  178. package/src/core/router/file-router.ts +0 -623
  179. package/src/core/router/index.ts +0 -260
  180. package/src/core/router/radix-tree.ts +0 -242
  181. package/src/core/serializer.ts +0 -397
  182. package/src/core/store/index.ts +0 -30
  183. package/src/core/store/registry.ts +0 -178
  184. package/src/core/store/request-store.ts +0 -240
  185. package/src/core/store/types.ts +0 -233
  186. package/src/core/types.ts +0 -616
  187. package/src/database/adapter.ts +0 -35
  188. package/src/database/adapters/index.ts +0 -1
  189. package/src/database/adapters/mysql.ts +0 -669
  190. package/src/database/database.ts +0 -70
  191. package/src/database/dialect.ts +0 -388
  192. package/src/database/index.ts +0 -12
  193. package/src/database/migrations.ts +0 -86
  194. package/src/database/optimizer.ts +0 -125
  195. package/src/database/query-builder.ts +0 -404
  196. package/src/database/realtime.ts +0 -53
  197. package/src/database/schema.ts +0 -71
  198. package/src/database/transactions.ts +0 -56
  199. package/src/database/types.ts +0 -87
  200. package/src/deployment/cluster.ts +0 -471
  201. package/src/deployment/config.ts +0 -454
  202. package/src/deployment/docker.ts +0 -599
  203. package/src/deployment/graceful-shutdown.ts +0 -373
  204. package/src/deployment/index.ts +0 -56
  205. package/src/index.ts +0 -281
  206. package/src/security/adapter.ts +0 -318
  207. package/src/security/auth/JWTPlugin.ts +0 -234
  208. package/src/security/auth/JWTProvider.ts +0 -316
  209. package/src/security/auth/adapter.ts +0 -12
  210. package/src/security/auth/jwt.ts +0 -234
  211. package/src/security/auth/middleware.ts +0 -188
  212. package/src/security/csrf.ts +0 -220
  213. package/src/security/headers.ts +0 -108
  214. package/src/security/index.ts +0 -60
  215. package/src/security/rate-limit/adapter.ts +0 -7
  216. package/src/security/rate-limit/memory.ts +0 -108
  217. package/src/security/rate-limit/middleware.ts +0 -181
  218. package/src/security/sanitization.ts +0 -75
  219. package/src/security/types.ts +0 -240
  220. package/src/security/utils.ts +0 -52
  221. 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
- }