millas 0.2.13 → 0.2.14

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 (88) hide show
  1. package/package.json +6 -3
  2. package/src/admin/Admin.js +107 -1027
  3. package/src/admin/AdminAuth.js +1 -1
  4. package/src/admin/ViewContext.js +1 -1
  5. package/src/admin/handlers/ActionHandler.js +103 -0
  6. package/src/admin/handlers/ApiHandler.js +113 -0
  7. package/src/admin/handlers/AuthHandler.js +76 -0
  8. package/src/admin/handlers/ExportHandler.js +70 -0
  9. package/src/admin/handlers/InlineHandler.js +71 -0
  10. package/src/admin/handlers/PageHandler.js +351 -0
  11. package/src/admin/resources/AdminResource.js +22 -1
  12. package/src/admin/static/SelectFilter2.js +34 -0
  13. package/src/admin/static/actions.js +201 -0
  14. package/src/admin/static/admin.css +7 -0
  15. package/src/admin/static/change_form.js +585 -0
  16. package/src/admin/static/core.js +128 -0
  17. package/src/admin/static/login.js +76 -0
  18. package/src/admin/static/vendor/bi/bootstrap-icons.min.css +5 -0
  19. package/src/admin/static/vendor/bi/fonts/bootstrap-icons.woff +0 -0
  20. package/src/admin/static/vendor/bi/fonts/bootstrap-icons.woff2 +0 -0
  21. package/src/admin/static/vendor/jquery.min.js +2 -0
  22. package/src/admin/views/layouts/base.njk +30 -113
  23. package/src/admin/views/pages/detail.njk +10 -9
  24. package/src/admin/views/pages/form.njk +4 -4
  25. package/src/admin/views/pages/list.njk +11 -193
  26. package/src/admin/views/pages/login.njk +19 -64
  27. package/src/admin/views/partials/form-field.njk +1 -1
  28. package/src/admin/views/partials/form-scripts.njk +4 -478
  29. package/src/admin/views/partials/form-widget.njk +10 -10
  30. package/src/ai/AITokenBudget.js +1 -1
  31. package/src/auth/Auth.js +112 -3
  32. package/src/auth/AuthMiddleware.js +18 -15
  33. package/src/auth/Hasher.js +15 -43
  34. package/src/cli.js +3 -0
  35. package/src/commands/call.js +190 -0
  36. package/src/commands/createsuperuser.js +3 -4
  37. package/src/commands/key.js +97 -0
  38. package/src/commands/make.js +16 -2
  39. package/src/commands/new.js +16 -1
  40. package/src/commands/serve.js +5 -5
  41. package/src/console/Command.js +337 -0
  42. package/src/console/CommandLoader.js +165 -0
  43. package/src/console/index.js +6 -0
  44. package/src/container/AppInitializer.js +48 -1
  45. package/src/container/Application.js +3 -1
  46. package/src/container/HttpServer.js +0 -1
  47. package/src/container/MillasConfig.js +48 -0
  48. package/src/controller/Controller.js +13 -11
  49. package/src/core/docs.js +6 -0
  50. package/src/core/foundation.js +8 -0
  51. package/src/core/http.js +20 -10
  52. package/src/core/validation.js +58 -27
  53. package/src/docs/Docs.js +268 -0
  54. package/src/docs/DocsServiceProvider.js +80 -0
  55. package/src/docs/SchemaInferrer.js +131 -0
  56. package/src/docs/handlers/ApiHandler.js +305 -0
  57. package/src/docs/handlers/PageHandler.js +47 -0
  58. package/src/docs/index.js +13 -0
  59. package/src/docs/resources/ApiResource.js +402 -0
  60. package/src/docs/static/docs.css +723 -0
  61. package/src/docs/static/docs.js +1181 -0
  62. package/src/encryption/Encrypter.js +381 -0
  63. package/src/facades/Auth.js +5 -2
  64. package/src/facades/Crypt.js +166 -0
  65. package/src/facades/Docs.js +43 -0
  66. package/src/facades/Mail.js +1 -1
  67. package/src/http/MillasRequest.js +7 -31
  68. package/src/http/RequestContext.js +11 -7
  69. package/src/http/SecurityBootstrap.js +24 -2
  70. package/src/http/Shape.js +168 -0
  71. package/src/http/adapters/ExpressAdapter.js +9 -5
  72. package/src/middleware/CorsMiddleware.js +3 -0
  73. package/src/middleware/ThrottleMiddleware.js +10 -7
  74. package/src/orm/model/Model.js +14 -1
  75. package/src/providers/EncryptionServiceProvider.js +66 -0
  76. package/src/router/MiddlewareRegistry.js +79 -54
  77. package/src/router/Route.js +9 -4
  78. package/src/router/RouteEntry.js +91 -0
  79. package/src/router/Router.js +71 -1
  80. package/src/scaffold/maker.js +138 -1
  81. package/src/scaffold/templates.js +12 -0
  82. package/src/serializer/Serializer.js +239 -0
  83. package/src/support/Str.js +1080 -0
  84. package/src/validation/BaseValidator.js +45 -5
  85. package/src/validation/Validator.js +67 -61
  86. package/src/validation/types.js +490 -0
  87. package/src/middleware/AuthMiddleware.js +0 -46
  88. package/src/middleware/MiddlewareRegistry.js +0 -106
@@ -0,0 +1,723 @@
1
+ /* ── Reset & Base ─────────────────────────────────────────────────────────── */
2
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
3
+
4
+ :root {
5
+ --bg: #f5f5f5;
6
+ --bg-2: #ffffff;
7
+ --bg-3: #f9f9f9;
8
+ --bg-4: #eeeeee;
9
+ --border: #e0e0e0;
10
+ --border-2: #d0d0d0;
11
+ --text: #2c2c2c;
12
+ --text-2: #5c5c5c;
13
+ --text-3: #999999;
14
+ --accent: #c47c4a;
15
+ --accent-bg: rgba(196,124,74,.06);
16
+ --green: #2d7a4f;
17
+ --orange: #d97706;
18
+ --red: #dc2626;
19
+ --purple: #9333ea;
20
+
21
+ --verb-get: #2d7a4f;
22
+ --verb-post: #c47c4a;
23
+ --verb-put: #d97706;
24
+ --verb-patch: #ea580c;
25
+ --verb-delete: #dc2626;
26
+
27
+ --sidebar-w: 280px;
28
+ --radius: 6px;
29
+ --mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
30
+ --shadow: 0 1px 2px rgba(0,0,0,.04), 0 1px 3px rgba(0,0,0,.03);
31
+ --shadow-md: 0 2px 8px rgba(0,0,0,.06);
32
+ }
33
+
34
+ body {
35
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
36
+ background: var(--bg);
37
+ color: var(--text);
38
+ height: 100vh;
39
+ overflow: hidden;
40
+ font-size: 13px;
41
+ line-height: 1.5;
42
+ }
43
+
44
+ #app { display: flex; height: 100vh; overflow: hidden; }
45
+
46
+ /* ── Sidebar ──────────────────────────────────────────────────────────────── */
47
+ .sidebar {
48
+ width: var(--sidebar-w);
49
+ min-width: var(--sidebar-w);
50
+ background: var(--bg-2);
51
+ border-right: 1px solid var(--border);
52
+ display: flex;
53
+ flex-direction: column;
54
+ overflow: hidden;
55
+ box-shadow: var(--shadow);
56
+ }
57
+
58
+ .sidebar-header {
59
+ padding: 14px 16px;
60
+ border-bottom: 1px solid var(--border);
61
+ display: flex;
62
+ align-items: center;
63
+ gap: 10px;
64
+ background: var(--bg-2);
65
+ }
66
+
67
+ .sidebar-header h1 {
68
+ font-size: 14px;
69
+ font-weight: 600;
70
+ color: var(--text);
71
+ overflow: hidden;
72
+ text-overflow: ellipsis;
73
+ white-space: nowrap;
74
+ }
75
+
76
+ .sidebar-logo {
77
+ width: 28px;
78
+ height: 28px;
79
+ background: var(--accent-bg);
80
+ border: 1px solid rgba(9,105,218,.2);
81
+ border-radius: var(--radius);
82
+ display: flex;
83
+ align-items: center;
84
+ justify-content: center;
85
+ color: var(--accent);
86
+ font-size: 14px;
87
+ flex-shrink: 0;
88
+ }
89
+
90
+ .sidebar-search {
91
+ padding: 10px 12px;
92
+ border-bottom: 1px solid var(--border);
93
+ background: var(--bg-2);
94
+ }
95
+
96
+ .sidebar-search input {
97
+ width: 100%;
98
+ background: var(--bg-3);
99
+ border: 1px solid var(--border);
100
+ border-radius: var(--radius);
101
+ color: var(--text);
102
+ padding: 6px 10px;
103
+ font-size: 12px;
104
+ outline: none;
105
+ transition: border-color .15s, box-shadow .15s;
106
+ }
107
+
108
+ .sidebar-search input:focus {
109
+ border-color: var(--accent);
110
+ box-shadow: 0 0 0 3px rgba(9,105,218,.1);
111
+ }
112
+
113
+ .sidebar-search input::placeholder { color: var(--text-3); }
114
+
115
+ .sidebar-scroll {
116
+ flex: 1;
117
+ overflow-y: auto;
118
+ padding: 6px 0;
119
+ background: var(--bg-2);
120
+ }
121
+
122
+ .sidebar-scroll::-webkit-scrollbar { width: 4px; }
123
+ .sidebar-scroll::-webkit-scrollbar-track { background: transparent; }
124
+ .sidebar-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
125
+
126
+ /* Groups */
127
+ .sidebar-group { margin-bottom: 2px; }
128
+
129
+ .sidebar-group-header {
130
+ display: flex;
131
+ align-items: center;
132
+ gap: 8px;
133
+ padding: 7px 12px;
134
+ cursor: pointer;
135
+ color: var(--text-2);
136
+ font-size: 11px;
137
+ font-weight: 600;
138
+ text-transform: uppercase;
139
+ letter-spacing: .5px;
140
+ user-select: none;
141
+ transition: color .15s, background .15s;
142
+ border-radius: 4px;
143
+ margin: 0 4px;
144
+ }
145
+
146
+ .sidebar-group-header:hover { color: var(--text); background: var(--bg-3); }
147
+
148
+ .sidebar-group-header .bi { font-size: 12px; }
149
+
150
+ .sidebar-group-chevron {
151
+ margin-left: auto;
152
+ transition: transform .2s;
153
+ font-size: 10px;
154
+ color: var(--text-3);
155
+ }
156
+
157
+ .sidebar-group.open .sidebar-group-chevron { transform: rotate(90deg); }
158
+
159
+ .sidebar-group-endpoints { display: none; }
160
+ .sidebar-group.open .sidebar-group-endpoints { display: block; }
161
+
162
+ /* Endpoint rows */
163
+ .sidebar-ep {
164
+ display: flex;
165
+ align-items: center;
166
+ gap: 8px;
167
+ padding: 5px 12px 5px 24px;
168
+ cursor: pointer;
169
+ transition: background .1s;
170
+ border-left: 2px solid transparent;
171
+ margin: 1px 0;
172
+ }
173
+
174
+ .sidebar-ep:hover { background: var(--bg-3); }
175
+
176
+ .sidebar-ep.active {
177
+ background: var(--accent-bg);
178
+ border-left-color: var(--accent);
179
+ }
180
+
181
+ .sidebar-ep .verb-badge {
182
+ font-size: 9px;
183
+ font-weight: 700;
184
+ font-family: var(--mono);
185
+ padding: 2px 5px;
186
+ border-radius: 4px;
187
+ min-width: 44px;
188
+ text-align: center;
189
+ flex-shrink: 0;
190
+ }
191
+
192
+ .sidebar-ep .ep-name {
193
+ font-size: 12px;
194
+ color: var(--text-2);
195
+ overflow: hidden;
196
+ text-overflow: ellipsis;
197
+ white-space: nowrap;
198
+ flex: 1;
199
+ }
200
+
201
+ .sidebar-ep.active .ep-name { color: var(--text); font-weight: 500; }
202
+
203
+ /* ── Verb badge colours ───────────────────────────────────────────────────── */
204
+ .verb-GET { background: rgba(26,127,55,.1); color: var(--verb-get); border: 1px solid rgba(26,127,55,.2); }
205
+ .verb-POST { background: rgba(9,105,218,.1); color: var(--verb-post); border: 1px solid rgba(9,105,218,.2); }
206
+ .verb-PUT { background: rgba(154,103,0,.1); color: var(--verb-put); border: 1px solid rgba(154,103,0,.2); }
207
+ .verb-PATCH { background: rgba(188,76,0,.1); color: var(--verb-patch); border: 1px solid rgba(188,76,0,.2); }
208
+ .verb-DELETE { background: rgba(207,34,46,.1); color: var(--verb-delete); border: 1px solid rgba(207,34,46,.2); }
209
+
210
+ /* ── Main area ────────────────────────────────────────────────────────────── */
211
+ .main {
212
+ flex: 1;
213
+ display: flex;
214
+ flex-direction: column;
215
+ overflow: hidden;
216
+ background: var(--bg);
217
+ }
218
+
219
+ /* Env bar */
220
+ .env-bar {
221
+ display: flex;
222
+ align-items: center;
223
+ gap: 8px;
224
+ padding: 8px 16px;
225
+ background: var(--bg-2);
226
+ border-bottom: 1px solid var(--border);
227
+ flex-shrink: 0;
228
+ box-shadow: var(--shadow);
229
+ }
230
+
231
+ .env-bar label {
232
+ font-size: 11px;
233
+ font-weight: 600;
234
+ color: var(--text-3);
235
+ white-space: nowrap;
236
+ text-transform: uppercase;
237
+ letter-spacing: .4px;
238
+ }
239
+
240
+ .env-bar input {
241
+ background: var(--bg-3);
242
+ border: 1px solid var(--border);
243
+ border-radius: var(--radius);
244
+ color: var(--text);
245
+ padding: 5px 9px;
246
+ font-size: 12px;
247
+ font-family: var(--mono);
248
+ outline: none;
249
+ transition: border-color .15s, box-shadow .15s;
250
+ }
251
+
252
+ .env-bar input:focus {
253
+ border-color: var(--accent);
254
+ box-shadow: 0 0 0 3px rgba(9,105,218,.1);
255
+ background: var(--bg-2);
256
+ }
257
+
258
+ .env-bar .base-url-input { flex: 1; min-width: 0; }
259
+ .env-bar .token-input { width: 260px; }
260
+
261
+ .env-bar .export-btn {
262
+ margin-left: auto;
263
+ display: flex;
264
+ align-items: center;
265
+ gap: 6px;
266
+ }
267
+
268
+ /* ── Detail panel ─────────────────────────────────────────────────────────── */
269
+ .detail {
270
+ flex: 1;
271
+ overflow-y: auto;
272
+ padding: 0;
273
+ background: var(--bg);
274
+ }
275
+
276
+ .detail::-webkit-scrollbar { width: 6px; }
277
+ .detail::-webkit-scrollbar-track { background: transparent; }
278
+ .detail::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
279
+
280
+ /* Empty state */
281
+ .empty-state {
282
+ display: flex;
283
+ flex-direction: column;
284
+ align-items: center;
285
+ justify-content: center;
286
+ height: 100%;
287
+ gap: 12px;
288
+ color: var(--text-3);
289
+ }
290
+
291
+ .empty-state .bi { font-size: 40px; color: var(--border-2); }
292
+ .empty-state p { font-size: 14px; color: var(--text-3); }
293
+
294
+ /* Endpoint header */
295
+ .ep-header {
296
+ padding: 20px 24px 16px;
297
+ border-bottom: 1px solid var(--border);
298
+ background: var(--bg-2);
299
+ box-shadow: var(--shadow);
300
+ }
301
+
302
+ .ep-header-top {
303
+ display: flex;
304
+ align-items: center;
305
+ gap: 10px;
306
+ margin-bottom: 8px;
307
+ flex-wrap: wrap;
308
+ }
309
+
310
+ .ep-header-top .verb-badge {
311
+ font-size: 11px;
312
+ font-weight: 700;
313
+ font-family: var(--mono);
314
+ padding: 4px 10px;
315
+ border-radius: var(--radius);
316
+ }
317
+
318
+ .ep-path {
319
+ font-family: var(--mono);
320
+ font-size: 15px;
321
+ color: var(--text);
322
+ font-weight: 500;
323
+ flex: 1;
324
+ }
325
+
326
+ .badge {
327
+ font-size: 10px;
328
+ font-weight: 600;
329
+ padding: 2px 8px;
330
+ border-radius: 20px;
331
+ letter-spacing: .2px;
332
+ }
333
+
334
+ .badge-auth { background: rgba(130,80,223,.08); color: var(--purple); border: 1px solid rgba(130,80,223,.25); }
335
+ .badge-public { background: rgba(26,127,55,.08); color: var(--green); border: 1px solid rgba(26,127,55,.2); }
336
+ .badge-deprecated { background: rgba(207,34,46,.08); color: var(--red); border: 1px solid rgba(207,34,46,.2); }
337
+ .badge-auto { background: var(--bg-3); color: var(--text-3); border: 1px solid var(--border); }
338
+
339
+ .ep-label {
340
+ font-size: 18px;
341
+ font-weight: 600;
342
+ color: var(--text);
343
+ margin-bottom: 4px;
344
+ }
345
+
346
+ .ep-description {
347
+ font-size: 13px;
348
+ color: var(--text-2);
349
+ line-height: 1.6;
350
+ }
351
+
352
+ /* ── Try bar ──────────────────────────────────────────────────────────────── */
353
+ .try-bar {
354
+ display: flex;
355
+ align-items: center;
356
+ gap: 8px;
357
+ padding: 12px 24px;
358
+ background: var(--bg-3);
359
+ border-bottom: 1px solid var(--border);
360
+ }
361
+
362
+ .try-url {
363
+ flex: 1;
364
+ background: var(--bg-2);
365
+ border: 1px solid var(--border);
366
+ border-radius: var(--radius);
367
+ color: var(--text);
368
+ padding: 8px 12px;
369
+ font-family: var(--mono);
370
+ font-size: 13px;
371
+ outline: none;
372
+ transition: border-color .15s, box-shadow .15s;
373
+ box-shadow: var(--shadow);
374
+ }
375
+
376
+ .try-url:focus {
377
+ border-color: var(--accent);
378
+ box-shadow: 0 0 0 3px rgba(9,105,218,.1);
379
+ }
380
+
381
+ .btn {
382
+ display: inline-flex;
383
+ align-items: center;
384
+ gap: 6px;
385
+ padding: 7px 14px;
386
+ border-radius: var(--radius);
387
+ border: none;
388
+ cursor: pointer;
389
+ font-size: 13px;
390
+ font-weight: 500;
391
+ transition: all .15s;
392
+ white-space: nowrap;
393
+ }
394
+
395
+ .btn-primary {
396
+ background: var(--accent);
397
+ color: #ffffff;
398
+ box-shadow: 0 1px 3px rgba(9,105,218,.3);
399
+ }
400
+
401
+ .btn-primary:hover { background: #b36d3f; box-shadow: 0 2px 6px rgba(196,124,74,.3); }
402
+ .btn-primary:disabled { opacity: .5; cursor: not-allowed; }
403
+
404
+ .btn-ghost {
405
+ background: var(--bg-2);
406
+ color: var(--text-2);
407
+ border: 1px solid var(--border);
408
+ }
409
+
410
+ .btn-ghost:hover { background: var(--bg-3); color: var(--text); border-color: var(--border-2); }
411
+
412
+ .btn-sm { padding: 4px 10px; font-size: 12px; }
413
+
414
+ /* ── Sections ─────────────────────────────────────────────────────────────── */
415
+ .section {
416
+ padding: 20px 24px;
417
+ border-bottom: 1px solid var(--border);
418
+ background: var(--bg-2);
419
+ margin-bottom: 8px;
420
+ border-radius: 0;
421
+ }
422
+
423
+ .section:last-child { border-bottom: none; margin-bottom: 0; }
424
+
425
+ .section-title {
426
+ font-size: 11px;
427
+ font-weight: 600;
428
+ text-transform: uppercase;
429
+ letter-spacing: .6px;
430
+ color: var(--text-3);
431
+ margin-bottom: 14px;
432
+ display: flex;
433
+ align-items: center;
434
+ gap: 6px;
435
+ }
436
+
437
+ /* Field table */
438
+ .field-table {
439
+ width: 100%;
440
+ border-collapse: collapse;
441
+ border: 1px solid var(--border);
442
+ border-radius: var(--radius);
443
+ overflow: hidden;
444
+ }
445
+
446
+ .field-table th {
447
+ font-size: 11px;
448
+ font-weight: 600;
449
+ color: var(--text-3);
450
+ text-align: left;
451
+ padding: 8px 12px;
452
+ background: var(--bg-3);
453
+ border-bottom: 1px solid var(--border);
454
+ text-transform: uppercase;
455
+ letter-spacing: .4px;
456
+ }
457
+
458
+ .field-table td {
459
+ padding: 8px 12px;
460
+ border-bottom: 1px solid var(--bg-3);
461
+ vertical-align: middle;
462
+ }
463
+
464
+ .field-table tr:last-child td { border-bottom: none; }
465
+ .field-table tr:hover td { background: var(--bg-3); }
466
+
467
+ .field-name {
468
+ font-family: var(--mono);
469
+ font-size: 12px;
470
+ color: var(--accent);
471
+ font-weight: 600;
472
+ }
473
+
474
+ .field-type {
475
+ font-size: 11px;
476
+ color: var(--text-3);
477
+ font-family: var(--mono);
478
+ background: var(--bg-3);
479
+ padding: 1px 6px;
480
+ border-radius: 3px;
481
+ border: 1px solid var(--border);
482
+ }
483
+
484
+ .field-required {
485
+ font-size: 10px;
486
+ font-weight: 600;
487
+ color: var(--red);
488
+ padding: 2px 6px;
489
+ border-radius: 20px;
490
+ background: rgba(207,34,46,.07);
491
+ border: 1px solid rgba(207,34,46,.2);
492
+ }
493
+
494
+ .field-input {
495
+ width: 100%;
496
+ background: var(--bg-2);
497
+ border: 1px solid var(--border);
498
+ border-radius: var(--radius);
499
+ color: var(--text);
500
+ padding: 6px 9px;
501
+ font-size: 12px;
502
+ font-family: var(--mono);
503
+ outline: none;
504
+ transition: border-color .15s, box-shadow .15s;
505
+ }
506
+
507
+ .field-input:focus {
508
+ border-color: var(--accent);
509
+ box-shadow: 0 0 0 3px rgba(9,105,218,.1);
510
+ }
511
+
512
+ .field-input.boolean-input { width: auto; cursor: pointer; transform: scale(1.2); }
513
+
514
+ .field-desc { font-size: 11px; color: var(--text-3); margin-top: 3px; }
515
+
516
+ /* ── Response panel ───────────────────────────────────────────────────────── */
517
+ .response-section {
518
+ padding: 16px 24px;
519
+ border-top: 2px solid var(--border);
520
+ background: var(--bg-2);
521
+ margin-top: 8px;
522
+ }
523
+
524
+ .response-meta {
525
+ display: flex;
526
+ align-items: center;
527
+ gap: 12px;
528
+ margin-bottom: 12px;
529
+ flex-wrap: wrap;
530
+ }
531
+
532
+ .status-badge {
533
+ font-family: var(--mono);
534
+ font-size: 12px;
535
+ font-weight: 700;
536
+ padding: 3px 10px;
537
+ border-radius: var(--radius);
538
+ }
539
+
540
+ .status-2xx { background: rgba(26,127,55,.1); color: var(--green); border: 1px solid rgba(26,127,55,.2); }
541
+ .status-3xx { background: rgba(9,105,218,.1); color: var(--accent); border: 1px solid rgba(9,105,218,.2); }
542
+ .status-4xx { background: rgba(154,103,0,.1); color: var(--orange); border: 1px solid rgba(154,103,0,.2); }
543
+ .status-5xx { background: rgba(207,34,46,.1); color: var(--red); border: 1px solid rgba(207,34,46,.2); }
544
+
545
+ .response-time { font-size: 11px; color: var(--text-3); display: flex; align-items: center; gap: 4px; }
546
+
547
+ .code-block {
548
+ background: #fafafa;
549
+ border: 1px solid var(--border);
550
+ border-radius: var(--radius);
551
+ padding: 14px 16px;
552
+ font-family: var(--mono);
553
+ font-size: 12px;
554
+ line-height: 1.7;
555
+ overflow: auto;
556
+ max-height: 400px;
557
+ color: var(--text);
558
+ white-space: pre;
559
+ box-shadow: inset 0 1px 3px rgba(0,0,0,.04);
560
+ }
561
+
562
+ .code-block::-webkit-scrollbar { width: 6px; height: 6px; }
563
+ .code-block::-webkit-scrollbar-track { background: transparent; }
564
+ .code-block::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 3px; }
565
+
566
+ /* ── Code tabs ────────────────────────────────────────────────────────────── */
567
+ .tabs {
568
+ display: flex;
569
+ gap: 0;
570
+ margin-bottom: 10px;
571
+ border-bottom: 2px solid var(--border);
572
+ }
573
+
574
+ .tab-btn {
575
+ background: transparent;
576
+ border: none;
577
+ color: var(--text-3);
578
+ padding: 7px 14px;
579
+ font-size: 12px;
580
+ font-weight: 500;
581
+ cursor: pointer;
582
+ border-bottom: 2px solid transparent;
583
+ margin-bottom: -2px;
584
+ transition: all .15s;
585
+ }
586
+
587
+ .tab-btn:hover { color: var(--text); }
588
+ .tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
589
+
590
+ .tab-panel { display: none; }
591
+ .tab-panel.active { display: block; }
592
+
593
+ /* ── Response docs ────────────────────────────────────────────────────────── */
594
+ .response-doc {
595
+ display: flex;
596
+ gap: 12px;
597
+ align-items: flex-start;
598
+ padding: 10px 0;
599
+ border-bottom: 1px solid var(--bg-3);
600
+ }
601
+
602
+ .response-doc:last-child { border-bottom: none; }
603
+
604
+ /* ── Spinner ──────────────────────────────────────────────────────────────── */
605
+ .spinner {
606
+ display: inline-block;
607
+ width: 14px;
608
+ height: 14px;
609
+ border: 2px solid rgba(9,105,218,.2);
610
+ border-top-color: var(--accent);
611
+ border-radius: 50%;
612
+ animation: spin .7s linear infinite;
613
+ }
614
+
615
+ @keyframes spin { to { transform: rotate(360deg); } }
616
+
617
+ /* ── Scrollbar global ─────────────────────────────────────────────────────── */
618
+ * { scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
619
+
620
+ /* ── Syntax highlighting ──────────────────────────────────────────────────── */
621
+ .syn-key { color: #c47c4a; font-weight: 600; }
622
+ .syn-string { color: #2d7a4f; }
623
+ .syn-number { color: #d97706; }
624
+ .syn-bool { color: #9333ea; font-weight: 600; }
625
+ .syn-null { color: #999999; font-style: italic; }
626
+
627
+ /* ── Sidebar ep path hint ─────────────────────────────────────────────────── */
628
+ .sidebar-ep { align-items: flex-start; padding: 6px 12px 6px 16px; }
629
+
630
+ .sidebar-ep-left { padding-top: 2px; flex-shrink: 0; }
631
+
632
+ .sidebar-ep-info {
633
+ flex: 1;
634
+ min-width: 0;
635
+ display: flex;
636
+ flex-direction: column;
637
+ gap: 1px;
638
+ }
639
+
640
+ .ep-path-hint {
641
+ font-family: var(--mono);
642
+ font-size: 10px;
643
+ color: var(--text-3);
644
+ overflow: hidden;
645
+ text-overflow: ellipsis;
646
+ white-space: nowrap;
647
+ }
648
+
649
+ .sidebar-ep.active .ep-path-hint { color: var(--accent); opacity: .7; }
650
+
651
+ /* ── Group count badge ────────────────────────────────────────────────────── */
652
+ .group-count-badge {
653
+ font-size: 10px;
654
+ font-weight: 600;
655
+ padding: 1px 6px;
656
+ border-radius: 20px;
657
+ background: var(--bg-3);
658
+ color: var(--text-3);
659
+ border: 1px solid var(--border);
660
+ margin-left: 4px;
661
+ }
662
+
663
+ /* ── Sidebar footer ───────────────────────────────────────────────────────── */
664
+ .sidebar-footer {
665
+ padding: 8px 14px;
666
+ border-top: 1px solid var(--border);
667
+ font-size: 11px;
668
+ color: var(--text-3);
669
+ background: var(--bg-2);
670
+ flex-shrink: 0;
671
+ }
672
+
673
+ /* ── Environment selector ─────────────────────────────────────────────────── */
674
+ .env-select {
675
+ background: var(--bg-3);
676
+ border: 1px solid var(--border);
677
+ border-radius: var(--radius);
678
+ color: var(--text);
679
+ padding: 4px 8px;
680
+ font-size: 12px;
681
+ outline: none;
682
+ cursor: pointer;
683
+ transition: border-color .15s;
684
+ }
685
+ .env-select:focus { border-color: var(--accent); }
686
+
687
+ /* ── Environment modal ────────────────────────────────────────────────────── */
688
+ /* Managed by UI.Modal from admin/static/ui.js — no CSS needed here */
689
+
690
+ /* ── Body mode toggle ─────────────────────────────────────────────────────── */
691
+ .body-mode-toggle {
692
+ display: flex;
693
+ border: 1px solid var(--border);
694
+ border-radius: var(--radius);
695
+ overflow: hidden;
696
+ }
697
+
698
+ .mode-btn {
699
+ background: var(--bg-3);
700
+ border: none;
701
+ color: var(--text-3);
702
+ padding: 3px 10px;
703
+ font-size: 11px;
704
+ font-weight: 500;
705
+ cursor: pointer;
706
+ transition: all .15s;
707
+ }
708
+
709
+ .mode-btn:hover { background: var(--bg-4); color: var(--text); }
710
+ .mode-btn.active { background: var(--accent); color: #fff; }
711
+
712
+ /* ── Raw body input ───────────────────────────────────────────────────────── */
713
+ .raw-body-input {
714
+ width: 100%;
715
+ min-height: 120px;
716
+ resize: vertical;
717
+ font-family: var(--mono);
718
+ font-size: 12px;
719
+ line-height: 1.6;
720
+ }
721
+
722
+ /* ── History row hover ────────────────────────────────────────────────────── */
723
+ .history-row:hover td { background: var(--bg-3) !important; }