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