4bnode 4.1.2 → 4.1.4

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 (62) hide show
  1. package/add-security.js +0 -18
  2. package/app-1/.4bnode/.passkey +1 -0
  3. package/app-1/.4bnode/assets/fonts/NotoSans-Bold.woff2 +0 -0
  4. package/app-1/.4bnode/assets/fonts/NotoSans-Light.woff2 +0 -0
  5. package/app-1/.4bnode/assets/fonts/NotoSans-Medium.woff2 +0 -0
  6. package/app-1/.4bnode/assets/fonts/NotoSans-Regular.woff2 +0 -0
  7. package/app-1/.4bnode/assets/fonts/NotoSans-SemiBold.woff2 +0 -0
  8. package/app-1/.4bnode/assets/images/mongo.png +0 -0
  9. package/app-1/.4bnode/assets/js/App.js +139 -0
  10. package/app-1/.4bnode/assets/js/Root.js +40 -0
  11. package/app-1/.4bnode/assets/js/components/Modal.js +52 -0
  12. package/app-1/.4bnode/assets/js/components/Sidebar.js +73 -0
  13. package/app-1/.4bnode/assets/js/components/ToastContainer.js +15 -0
  14. package/app-1/.4bnode/assets/js/context.js +7 -0
  15. package/app-1/.4bnode/assets/js/lib.js +113 -0
  16. package/app-1/.4bnode/assets/js/main.js +5 -0
  17. package/app-1/.4bnode/assets/js/pages/AiBuilderPage.js +268 -0
  18. package/app-1/.4bnode/assets/js/pages/ApiTesterPage.js +1879 -0
  19. package/app-1/.4bnode/assets/js/pages/DashboardPage.js +435 -0
  20. package/app-1/.4bnode/assets/js/pages/DatabasePage.js +300 -0
  21. package/app-1/.4bnode/assets/js/pages/DocsPage.js +150 -0
  22. package/app-1/.4bnode/assets/js/pages/EnvironmentPage.js +103 -0
  23. package/app-1/.4bnode/assets/js/pages/LogsPage.js +205 -0
  24. package/app-1/.4bnode/assets/js/pages/MailPage.js +134 -0
  25. package/app-1/.4bnode/assets/js/pages/RealtimePage.js +186 -0
  26. package/app-1/.4bnode/assets/js/pages/RoutesPage.js +1436 -0
  27. package/app-1/.4bnode/assets/js/pages/SchemasPage.js +838 -0
  28. package/app-1/.4bnode/assets/js/pages/SecurityPage.js +74 -0
  29. package/app-1/.4bnode/assets/js/pages/SerialPortPage.js +170 -0
  30. package/app-1/.4bnode/assets/js/screens/PasskeyScreen.js +105 -0
  31. package/app-1/.4bnode/codegen.js +1249 -0
  32. package/app-1/.4bnode/dev-api.js +3621 -0
  33. package/app-1/.4bnode/ui.html +1578 -0
  34. package/app-1/README.md +47 -0
  35. package/app-1/ecosystem.config.cjs +25 -0
  36. package/app-1/index.js +145 -0
  37. package/app-1/package.json +35 -0
  38. package/app-1/public/index.html +292 -0
  39. package/app-1/src/db.js +13 -0
  40. package/app-1/src/docs.js +84 -0
  41. package/app-1/src/middleware/errorHandler.js +15 -0
  42. package/app-1/src/middleware/roles.js +14 -0
  43. package/app-1/src/middleware/validate.js +20 -0
  44. package/app-1/src/models/login.js +9 -0
  45. package/app-1/src/openapi.json +62 -0
  46. package/app-1/src/routes/products.js +26 -0
  47. package/app-1/src/validators/products.js +7 -0
  48. package/app-1/src/validators/user.js +7 -0
  49. package/index.js +2 -17
  50. package/lib/codegen.js +91 -44
  51. package/lib/codegen.test.js +23 -3
  52. package/package.json +1 -1
  53. package/skeleton/.4bnode/assets/js/App.js +0 -2
  54. package/skeleton/.4bnode/assets/js/components/Sidebar.js +2 -3
  55. package/skeleton/.4bnode/assets/js/lib.js +0 -3
  56. package/skeleton/.4bnode/assets/js/pages/DocsPage.js +29 -21
  57. package/skeleton/.4bnode/assets/js/pages/RoutesPage.js +12 -0
  58. package/skeleton/.4bnode/codegen.js +91 -44
  59. package/skeleton/.4bnode/dev-api.js +173 -87
  60. package/skeleton/_gitignore +0 -1
  61. package/skeleton/index.js +4 -8
  62. package/skeleton/.4bnode/assets/js/pages/ApiKeysPage.js +0 -164
@@ -0,0 +1,1578 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>4bnode Dashboard</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ <style>
14
+ @font-face {
15
+ font-family: "Noto Sans";
16
+ src: url("/_dev/assets/fonts/NotoSans-Light.woff2") format("woff2");
17
+ font-weight: 300;
18
+ font-style: normal;
19
+ font-display: swap;
20
+ }
21
+ @font-face {
22
+ font-family: "Noto Sans";
23
+ src: url("/_dev/assets/fonts/NotoSans-Regular.woff2") format("woff2");
24
+ font-weight: 400;
25
+ font-style: normal;
26
+ font-display: swap;
27
+ }
28
+ @font-face {
29
+ font-family: "Noto Sans";
30
+ src: url("/_dev/assets/fonts/NotoSans-Medium.woff2") format("woff2");
31
+ font-weight: 500;
32
+ font-style: normal;
33
+ font-display: swap;
34
+ }
35
+ @font-face {
36
+ font-family: "Noto Sans";
37
+ src: url("/_dev/assets/fonts/NotoSans-SemiBold.woff2") format("woff2");
38
+ font-weight: 600;
39
+ font-style: normal;
40
+ font-display: swap;
41
+ }
42
+ @font-face {
43
+ font-family: "Noto Sans";
44
+ src: url("/_dev/assets/fonts/NotoSans-Bold.woff2") format("woff2");
45
+ font-weight: 700;
46
+ font-style: normal;
47
+ font-display: swap;
48
+ }
49
+
50
+ *,
51
+ *::before,
52
+ *::after {
53
+ margin: 0;
54
+ padding: 0;
55
+ box-sizing: border-box;
56
+ }
57
+
58
+ :root {
59
+ --bg: #141720;
60
+ --bg-surface: #11141e;
61
+ --bg-card: #1a1d2b;
62
+ --bg-card-hover: #21253a;
63
+ --bg-input: #13161f;
64
+ --border: rgba(255, 255, 255, 0.08);
65
+ --border-focus: rgba(0, 121, 255, 0.5);
66
+ --text: #eceef4;
67
+ --text-secondary: #8b8fa3;
68
+ --text-muted: #626880;
69
+ --accent: #0079ff;
70
+ --accent-light: #3daefd;
71
+ --accent-glow: rgba(0, 94, 255, 0.18);
72
+ --success: #22c55e;
73
+ --success-glow: rgba(34, 197, 94, 0.12);
74
+ --warning: #f59e0b;
75
+ --warning-glow: rgba(245, 158, 11, 0.12);
76
+ --error: #ef4444;
77
+ --error-glow: rgba(239, 68, 68, 0.12);
78
+ --info: #3b82f6;
79
+ --info-glow: rgba(59, 130, 246, 0.12);
80
+ --radius: 8px;
81
+ --radius-lg: 12px;
82
+ --radius-xl: 16px;
83
+ --sidebar-w: 220px;
84
+ --transition: 150ms ease;
85
+ }
86
+
87
+ html,
88
+ body {
89
+ height: 100%;
90
+ }
91
+ body {
92
+ font-family:
93
+ "Noto Sans",
94
+ -apple-system,
95
+ BlinkMacSystemFont,
96
+ sans-serif;
97
+ background: var(--bg);
98
+ color: var(--text);
99
+ overflow: hidden;
100
+ -webkit-font-smoothing: antialiased;
101
+ }
102
+
103
+ #root {
104
+ display: flex;
105
+ height: 100vh;
106
+ }
107
+
108
+ /* ── Scrollbar ── */
109
+ ::-webkit-scrollbar {
110
+ width: 6px;
111
+ }
112
+ ::-webkit-scrollbar-track {
113
+ background: transparent;
114
+ }
115
+ ::-webkit-scrollbar-thumb {
116
+ background: rgba(255, 255, 255, 0.08);
117
+ border-radius: 3px;
118
+ }
119
+ ::-webkit-scrollbar-thumb:hover {
120
+ background: rgba(255, 255, 255, 0.14);
121
+ }
122
+
123
+ /* ── Sidebar ── */
124
+ .sidebar {
125
+ width: var(--sidebar-w);
126
+ background: var(--bg-surface);
127
+ border-right: 1px solid var(--border);
128
+ display: flex;
129
+ flex-direction: column;
130
+ flex-shrink: 0;
131
+ padding: 0 0 16px;
132
+ overflow-y: auto;
133
+ }
134
+ .sidebar-brand {
135
+ height: 61px;
136
+ display: flex;
137
+ align-items: center;
138
+ padding: 0 20px;
139
+ border-bottom: 1px solid var(--border);
140
+ margin-bottom: 8px;
141
+ flex-shrink: 0;
142
+ }
143
+ .sidebar-brand pre {
144
+ font-family: monospace;
145
+ font-size: 4px;
146
+ line-height: 1.1;
147
+ letter-spacing: 0.5px;
148
+ background: linear-gradient(135deg, #3daefd, #0079ff, #005eff);
149
+ -webkit-background-clip: text;
150
+ -webkit-text-fill-color: transparent;
151
+ margin: 0 0 0 -2px;
152
+ white-space: pre;
153
+ user-select: none;
154
+ }
155
+ .sidebar-brand span {
156
+ font-size: 11px;
157
+ color: var(--text-muted);
158
+ font-weight: 400;
159
+ }
160
+ .sidebar-section {
161
+ padding: 12px 12px 4px;
162
+ font-size: 10px;
163
+ font-weight: 600;
164
+ color: var(--text-muted);
165
+ text-transform: uppercase;
166
+ letter-spacing: 1px;
167
+ }
168
+ .sidebar-item {
169
+ display: flex;
170
+ align-items: center;
171
+ gap: 10px;
172
+ padding: 9px 16px;
173
+ margin: 1px 8px;
174
+ border-radius: var(--radius);
175
+ font-size: 13px;
176
+ font-weight: 500;
177
+ color: var(--text-secondary);
178
+ cursor: pointer;
179
+ transition: all var(--transition);
180
+ position: relative;
181
+ }
182
+ .sidebar-item:hover {
183
+ background: rgba(255, 255, 255, 0.04);
184
+ color: var(--text);
185
+ }
186
+ .sidebar-item.active {
187
+ background: rgba(255, 255, 255, 0.06);
188
+ color: #f0f1f5;
189
+ }
190
+ .sidebar-item.active::before {
191
+ content: "";
192
+ position: absolute;
193
+ left: -8px;
194
+ top: 50%;
195
+ transform: translateY(-50%);
196
+ width: 3px;
197
+ height: 16px;
198
+ border-radius: 0 3px 3px 0;
199
+ background: var(--accent-light);
200
+ }
201
+ .sidebar-item.active .sidebar-icon {
202
+ color: var(--accent-light);
203
+ }
204
+ .sidebar-item.locked {
205
+ opacity: 0.4;
206
+ cursor: not-allowed;
207
+ }
208
+ .sidebar-item .lock-icon {
209
+ position: absolute;
210
+ right: 12px;
211
+ font-size: 10px;
212
+ opacity: 0.6;
213
+ }
214
+ .sidebar-icon {
215
+ width: 18px;
216
+ height: 18px;
217
+ display: flex;
218
+ align-items: center;
219
+ justify-content: center;
220
+ flex-shrink: 0;
221
+ }
222
+ .sidebar-icon svg {
223
+ width: 16px;
224
+ height: 16px;
225
+ stroke: currentColor;
226
+ fill: none;
227
+ }
228
+
229
+ /* ── Main Area ── */
230
+ .main {
231
+ flex: 1;
232
+ display: flex;
233
+ flex-direction: column;
234
+ overflow: hidden;
235
+ }
236
+ .topbar {
237
+ height: 61px;
238
+ display: flex;
239
+ align-items: center;
240
+ justify-content: space-between;
241
+ padding: 0 28px;
242
+ background: var(--bg-surface);
243
+ border-bottom: 1px solid var(--border);
244
+ flex-shrink: 0;
245
+ }
246
+ .topbar h2 {
247
+ font-size: 16px;
248
+ font-weight: 600;
249
+ }
250
+ .topbar-badge {
251
+ font-size: 11px;
252
+ padding: 3px 10px;
253
+ border-radius: 20px;
254
+ background: var(--accent-glow);
255
+ color: var(--accent-light);
256
+ font-weight: 500;
257
+ }
258
+ .content {
259
+ flex: 1;
260
+ overflow-y: auto;
261
+ padding: 28px;
262
+ }
263
+
264
+ /* ── Cards ── */
265
+ .card {
266
+ background: var(--bg-card);
267
+ border: 1px solid var(--border);
268
+ border-radius: var(--radius-lg);
269
+ padding: 20px;
270
+ transition: all var(--transition);
271
+ }
272
+ .card:hover {
273
+ border-color: rgba(255, 255, 255, 0.1);
274
+ }
275
+ .card-header {
276
+ display: flex;
277
+ align-items: center;
278
+ justify-content: space-between;
279
+ margin-bottom: 16px;
280
+ }
281
+ .card-title {
282
+ font-size: 14px;
283
+ font-weight: 600;
284
+ }
285
+ .card-subtitle {
286
+ font-size: 12px;
287
+ color: var(--text-muted);
288
+ margin-top: 2px;
289
+ }
290
+
291
+ /* ── Stat Cards ── */
292
+ .stats-grid {
293
+ display: grid;
294
+ grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
295
+ gap: 16px;
296
+ margin-bottom: 24px;
297
+ }
298
+ .stat-card {
299
+ background: var(--bg-card);
300
+ border: 1px solid var(--border);
301
+ border-radius: var(--radius-lg);
302
+ padding: 18px 20px;
303
+ display: flex;
304
+ flex-direction: column;
305
+ gap: 8px;
306
+ }
307
+ .stat-card .stat-label {
308
+ font-size: 12px;
309
+ color: var(--text-muted);
310
+ font-weight: 500;
311
+ }
312
+ .stat-card .stat-value {
313
+ font-size: 28px;
314
+ font-weight: 700;
315
+ letter-spacing: -1px;
316
+ }
317
+ .stat-card .stat-badge {
318
+ display: inline-flex;
319
+ align-items: center;
320
+ gap: 4px;
321
+ font-size: 11px;
322
+ font-weight: 500;
323
+ padding: 2px 8px;
324
+ border-radius: 20px;
325
+ width: fit-content;
326
+ }
327
+ .stat-badge.success {
328
+ background: var(--success-glow);
329
+ color: var(--success);
330
+ }
331
+ .stat-badge.warning {
332
+ background: var(--warning-glow);
333
+ color: var(--warning);
334
+ }
335
+ .stat-badge.error {
336
+ background: var(--error-glow);
337
+ color: var(--error);
338
+ }
339
+ .stat-badge.info {
340
+ background: var(--info-glow);
341
+ color: var(--info);
342
+ }
343
+
344
+ /* ── Buttons ── */
345
+ .btn {
346
+ display: inline-flex;
347
+ align-items: center;
348
+ gap: 6px;
349
+ padding: 8px 16px;
350
+ border-radius: var(--radius);
351
+ font-size: 13px;
352
+ font-weight: 500;
353
+ font-family: inherit;
354
+ border: none;
355
+ cursor: pointer;
356
+ transition: all var(--transition);
357
+ white-space: nowrap;
358
+ }
359
+ .btn:disabled {
360
+ opacity: 0.5;
361
+ cursor: not-allowed;
362
+ }
363
+ .btn-primary {
364
+ background: var(--accent);
365
+ color: white;
366
+ box-shadow: 0 0 20px var(--accent-glow);
367
+ }
368
+ .btn-primary:hover:not(:disabled) {
369
+ background: var(--accent-light);
370
+ }
371
+ .btn-secondary {
372
+ background: rgba(255, 255, 255, 0.06);
373
+ color: var(--text);
374
+ border: 1px solid var(--border);
375
+ }
376
+ .btn-secondary:hover:not(:disabled) {
377
+ background: rgba(255, 255, 255, 0.1);
378
+ border-color: rgba(255, 255, 255, 0.12);
379
+ }
380
+ .btn-ghost {
381
+ background: transparent;
382
+ color: var(--text-secondary);
383
+ }
384
+ .btn-ghost:hover:not(:disabled) {
385
+ background: rgba(255, 255, 255, 0.04);
386
+ color: var(--text);
387
+ }
388
+ .btn-danger {
389
+ background: var(--error-glow);
390
+ color: var(--error);
391
+ border: 1px solid rgba(239, 68, 68, 0.2);
392
+ }
393
+ .btn-danger:hover:not(:disabled) {
394
+ background: rgba(239, 68, 68, 0.2);
395
+ }
396
+ .btn-sm {
397
+ padding: 5px 10px;
398
+ font-size: 12px;
399
+ }
400
+ .btn-icon {
401
+ padding: 6px;
402
+ }
403
+
404
+ /* ── Form elements ── */
405
+ .form-group {
406
+ margin-bottom: 16px;
407
+ }
408
+ .form-label {
409
+ display: block;
410
+ font-size: 12px;
411
+ font-weight: 500;
412
+ color: var(--text-secondary);
413
+ margin-bottom: 6px;
414
+ }
415
+ .form-input,
416
+ .form-select {
417
+ width: 100%;
418
+ padding: 9px 12px;
419
+ background: var(--bg-input);
420
+ border: 1px solid var(--border);
421
+ border-radius: var(--radius);
422
+ color: var(--text);
423
+ font-size: 13px;
424
+ font-family: inherit;
425
+ outline: none;
426
+ transition: border-color var(--transition);
427
+ }
428
+ .form-input:focus,
429
+ .form-select:focus {
430
+ border-color: var(--border-focus);
431
+ }
432
+ .form-input::placeholder {
433
+ color: var(--text-muted);
434
+ }
435
+ .form-select option {
436
+ background: var(--bg-card);
437
+ }
438
+
439
+ .form-toggle {
440
+ position: relative;
441
+ width: 36px;
442
+ height: 20px;
443
+ display: inline-block;
444
+ }
445
+ .form-toggle input {
446
+ opacity: 0;
447
+ width: 0;
448
+ height: 0;
449
+ }
450
+ .form-toggle .slider {
451
+ position: absolute;
452
+ cursor: pointer;
453
+ inset: 0;
454
+ background: rgba(255, 255, 255, 0.1);
455
+ border-radius: 20px;
456
+ transition: var(--transition);
457
+ }
458
+ .form-toggle .slider::before {
459
+ content: "";
460
+ position: absolute;
461
+ width: 14px;
462
+ height: 14px;
463
+ left: 3px;
464
+ bottom: 3px;
465
+ background: white;
466
+ border-radius: 50%;
467
+ transition: var(--transition);
468
+ }
469
+ .form-toggle input:checked + .slider {
470
+ background: var(--accent);
471
+ }
472
+ .form-toggle input:checked + .slider::before {
473
+ transform: translateX(16px);
474
+ }
475
+
476
+ /* ── Grid layouts ── */
477
+ .grid-2 {
478
+ display: grid;
479
+ grid-template-columns: 1fr 1fr;
480
+ gap: 16px;
481
+ }
482
+ .grid-3 {
483
+ display: grid;
484
+ grid-template-columns: repeat(3, 1fr);
485
+ gap: 16px;
486
+ }
487
+ .grid-4 {
488
+ display: grid;
489
+ grid-template-columns: repeat(4, 1fr);
490
+ gap: 12px;
491
+ }
492
+
493
+ /* ── Badge ── */
494
+ .badge {
495
+ display: inline-flex;
496
+ align-items: center;
497
+ gap: 4px;
498
+ font-size: 11px;
499
+ font-weight: 500;
500
+ padding: 3px 8px;
501
+ border-radius: 20px;
502
+ background: rgba(255, 255, 255, 0.07);
503
+ color: var(--text-secondary);
504
+ }
505
+ .badge-success {
506
+ background: var(--success-glow);
507
+ color: var(--success);
508
+ }
509
+ .badge-warning {
510
+ background: var(--warning-glow);
511
+ color: var(--warning);
512
+ }
513
+ .badge-error {
514
+ background: var(--error-glow);
515
+ color: var(--error);
516
+ }
517
+ .badge-info {
518
+ background: var(--info-glow);
519
+ color: var(--info);
520
+ }
521
+ .badge-muted {
522
+ background: rgba(255, 255, 255, 0.05);
523
+ color: var(--text-muted);
524
+ }
525
+
526
+ /* ── DB Cards ── */
527
+ .db-card {
528
+ background: var(--bg-card);
529
+ border: 1px solid var(--border);
530
+ border-radius: var(--radius-lg);
531
+ padding: 24px;
532
+ text-align: center;
533
+ cursor: pointer;
534
+ transition: all var(--transition);
535
+ position: relative;
536
+ overflow: hidden;
537
+ }
538
+ .db-card:hover:not(.disabled) {
539
+ border-color: var(--accent);
540
+ box-shadow: 0 0 30px var(--accent-glow);
541
+ transform: translateY(-2px);
542
+ }
543
+ .db-card.active {
544
+ border-color: var(--accent);
545
+ box-shadow: 0 0 30px var(--accent-glow);
546
+ }
547
+ .db-card.disabled {
548
+ opacity: 0.5;
549
+ cursor: not-allowed;
550
+ }
551
+ .db-card .db-icon {
552
+ width: 36px;
553
+ height: 36px;
554
+ margin: 0 auto 10px;
555
+ display: flex;
556
+ align-items: center;
557
+ justify-content: center;
558
+ }
559
+ .db-card .db-name {
560
+ font-size: 14px;
561
+ font-weight: 600;
562
+ margin-bottom: 4px;
563
+ }
564
+ .db-card .db-desc {
565
+ font-size: 11px;
566
+ color: var(--text-muted);
567
+ }
568
+ .db-card .coming-soon {
569
+ position: absolute;
570
+ top: 8px;
571
+ right: 8px;
572
+ font-size: 9px;
573
+ font-weight: 600;
574
+ text-transform: uppercase;
575
+ letter-spacing: 0.5px;
576
+ padding: 2px 8px;
577
+ border-radius: 20px;
578
+ background: var(--warning-glow);
579
+ color: var(--warning);
580
+ }
581
+
582
+ /* ── Empty State ── */
583
+ .empty-state {
584
+ text-align: center;
585
+ padding: 48px 24px;
586
+ color: var(--text-muted);
587
+ }
588
+ .empty-state .empty-icon {
589
+ font-size: 36px;
590
+ margin-bottom: 12px;
591
+ opacity: 0.5;
592
+ display: flex;
593
+ align-items: center;
594
+ justify-content: center;
595
+ color: var(--text-muted);
596
+ }
597
+ .empty-state h3 {
598
+ font-size: 15px;
599
+ font-weight: 600;
600
+ color: var(--text-secondary);
601
+ margin-bottom: 6px;
602
+ }
603
+ .empty-state p {
604
+ font-size: 13px;
605
+ margin-bottom: 16px;
606
+ }
607
+
608
+ /* ── Locked State ── */
609
+ .locked-state {
610
+ text-align: center;
611
+ padding: 60px 24px;
612
+ }
613
+ .locked-state .locked-icon {
614
+ width: 56px;
615
+ height: 56px;
616
+ border-radius: 50%;
617
+ background: rgba(255, 255, 255, 0.04);
618
+ display: inline-flex;
619
+ align-items: center;
620
+ justify-content: center;
621
+ font-size: 24px;
622
+ margin-bottom: 16px;
623
+ }
624
+ .locked-state h3 {
625
+ font-size: 16px;
626
+ font-weight: 600;
627
+ margin-bottom: 6px;
628
+ }
629
+ .locked-state p {
630
+ font-size: 13px;
631
+ color: var(--text-muted);
632
+ margin-bottom: 20px;
633
+ max-width: 360px;
634
+ margin-left: auto;
635
+ margin-right: auto;
636
+ }
637
+
638
+ /* ── Table ── */
639
+ .table-wrap {
640
+ overflow-x: auto;
641
+ }
642
+ table {
643
+ width: 100%;
644
+ border-collapse: collapse;
645
+ }
646
+ th {
647
+ text-align: left;
648
+ font-size: 11px;
649
+ font-weight: 600;
650
+ color: var(--text-muted);
651
+ text-transform: uppercase;
652
+ letter-spacing: 0.5px;
653
+ padding: 10px 14px;
654
+ border-bottom: 1px solid var(--border);
655
+ }
656
+ td {
657
+ padding: 12px 14px;
658
+ font-size: 13px;
659
+ border-bottom: 1px solid var(--border);
660
+ }
661
+ tr:last-child td {
662
+ border-bottom: none;
663
+ }
664
+ tr:hover td {
665
+ background: rgba(255, 255, 255, 0.02);
666
+ }
667
+
668
+ /* ── Schema Builder (full page) ── */
669
+ .schema-builder {
670
+ display: grid;
671
+ grid-template-columns: 1fr 340px;
672
+ gap: 20px;
673
+ min-height: calc(100vh - 160px);
674
+ }
675
+ .schema-builder-fields {
676
+ display: flex;
677
+ flex-direction: column;
678
+ }
679
+ .schema-builder-sidebar {
680
+ position: sticky;
681
+ top: 0;
682
+ align-self: start;
683
+ }
684
+ .field-table {
685
+ width: 100%;
686
+ border-collapse: collapse;
687
+ }
688
+ .field-table th {
689
+ text-align: left;
690
+ font-size: 10px;
691
+ font-weight: 600;
692
+ color: var(--text-muted);
693
+ text-transform: uppercase;
694
+ letter-spacing: 0.8px;
695
+ padding: 8px 12px;
696
+ border-bottom: 1px solid var(--border);
697
+ position: sticky;
698
+ top: 0;
699
+ background: var(--bg-card);
700
+ z-index: 1;
701
+ }
702
+ .field-table td {
703
+ padding: 6px 12px;
704
+ font-size: 13px;
705
+ border-bottom: 1px solid rgba(255, 255, 255, 0.03);
706
+ vertical-align: middle;
707
+ }
708
+ .field-table tr:hover td {
709
+ background: rgba(255, 255, 255, 0.02);
710
+ }
711
+ .field-table .ft-num {
712
+ color: var(--text-muted);
713
+ font-size: 11px;
714
+ width: 32px;
715
+ font-variant-numeric: tabular-nums;
716
+ }
717
+ .field-table .ft-name {
718
+ font-weight: 500;
719
+ }
720
+ .field-table .ft-type {
721
+ color: var(--accent-light);
722
+ font-size: 12px;
723
+ }
724
+ .field-table .ft-toggle {
725
+ width: 28px;
726
+ height: 16px;
727
+ border-radius: 8px;
728
+ border: none;
729
+ cursor: pointer;
730
+ transition: all var(--transition);
731
+ }
732
+ .ft-toggle.on {
733
+ background: var(--success);
734
+ }
735
+ .ft-toggle.off {
736
+ background: rgba(255, 255, 255, 0.08);
737
+ }
738
+ .field-count {
739
+ font-size: 11px;
740
+ color: var(--text-muted);
741
+ font-variant-numeric: tabular-nums;
742
+ padding: 2px 8px;
743
+ background: rgba(255, 255, 255, 0.04);
744
+ border-radius: 20px;
745
+ }
746
+
747
+ /* Old field-row kept for schema cards in list view */
748
+ .field-row {
749
+ display: flex;
750
+ align-items: center;
751
+ gap: 10px;
752
+ padding: 8px 12px;
753
+ background: var(--bg-surface);
754
+ border: 1px solid var(--border);
755
+ border-radius: var(--radius);
756
+ margin-bottom: 4px;
757
+ font-size: 13px;
758
+ }
759
+ .field-row .field-name {
760
+ font-weight: 500;
761
+ min-width: 80px;
762
+ }
763
+ .field-row .field-type {
764
+ font-size: 12px;
765
+ color: var(--accent-light);
766
+ min-width: 60px;
767
+ }
768
+ .field-row .field-badges {
769
+ display: flex;
770
+ gap: 4px;
771
+ flex: 1;
772
+ }
773
+
774
+ /* ── Route Page Layout ── */
775
+ .routes-layout {
776
+ display: grid;
777
+ grid-template-columns: 340px 1fr;
778
+ gap: 20px;
779
+ min-height: calc(100vh - 160px);
780
+ }
781
+ .routes-list {
782
+ display: flex;
783
+ flex-direction: column;
784
+ gap: 0;
785
+ }
786
+ .routes-detail {
787
+ position: sticky;
788
+ top: 0;
789
+ align-self: start;
790
+ }
791
+ .route-item {
792
+ display: flex;
793
+ align-items: center;
794
+ gap: 10px;
795
+ padding: 12px 16px;
796
+ cursor: pointer;
797
+ border-left: 2px solid transparent;
798
+ transition: all var(--transition);
799
+ }
800
+ .route-item:hover {
801
+ background: rgba(255, 255, 255, 0.02);
802
+ }
803
+ .route-item.active {
804
+ background: var(--accent-glow);
805
+ border-left-color: var(--accent);
806
+ }
807
+ .route-item .ri-name {
808
+ font-weight: 500;
809
+ font-size: 13px;
810
+ flex: 1;
811
+ }
812
+ .route-item .ri-file {
813
+ font-size: 10px;
814
+ color: var(--text-muted);
815
+ }
816
+ .route-method {
817
+ font-size: 9px;
818
+ font-weight: 700;
819
+ letter-spacing: 0.5px;
820
+ padding: 2px 7px;
821
+ border-radius: 3px;
822
+ font-family: "SF Mono", "Fira Code", monospace;
823
+ text-align: center;
824
+ display: inline-block;
825
+ }
826
+ .route-method.get {
827
+ background: rgba(34, 197, 94, 0.15);
828
+ color: #22c55e;
829
+ }
830
+ .route-method.post {
831
+ background: rgba(59, 130, 246, 0.15);
832
+ color: #3b82f6;
833
+ }
834
+ .route-method.put {
835
+ background: rgba(245, 158, 11, 0.15);
836
+ color: #f59e0b;
837
+ }
838
+ .route-method.patch {
839
+ background: rgba(168, 85, 247, 0.15);
840
+ color: #a855f7;
841
+ }
842
+ .route-method.delete {
843
+ background: rgba(239, 68, 68, 0.15);
844
+ color: #ef4444;
845
+ }
846
+ .route-method.multi {
847
+ background: rgba(0, 94, 255, 0.15);
848
+ color: #3daefd;
849
+ }
850
+ .route-endpoint-row {
851
+ display: flex;
852
+ align-items: center;
853
+ gap: 10px;
854
+ padding: 10px 14px;
855
+ border-bottom: 1px solid rgba(255, 255, 255, 0.03);
856
+ font-size: 13px;
857
+ }
858
+ .route-endpoint-row:last-child {
859
+ border-bottom: none;
860
+ }
861
+ .route-endpoint-path {
862
+ font-family: "SF Mono", "Fira Code", monospace;
863
+ font-size: 12px;
864
+ color: var(--text-secondary);
865
+ }
866
+ .route-code-wrap {
867
+ max-height: 400px;
868
+ overflow-y: auto;
869
+ border-radius: var(--radius);
870
+ border: 1px solid var(--border);
871
+ }
872
+ .route-code-wrap .code-block {
873
+ border: none;
874
+ border-radius: 0;
875
+ margin: 0;
876
+ font-size: 11px;
877
+ line-height: 1.6;
878
+ }
879
+
880
+ /* ── API Tester ── */
881
+ .api-tester-bar {
882
+ display: flex;
883
+ gap: 8px;
884
+ align-items: stretch;
885
+ margin-bottom: 16px;
886
+ }
887
+ .api-tester-method {
888
+ padding: 8px 12px;
889
+ border: 1px solid var(--border);
890
+ border-radius: var(--radius);
891
+ background: var(--bg-input);
892
+ color: var(--text-primary);
893
+ font-weight: 600;
894
+ font-size: 13px;
895
+ font-family: inherit;
896
+ cursor: pointer;
897
+ min-width: 100px;
898
+ }
899
+ .api-tester-url {
900
+ flex: 1;
901
+ padding: 8px 14px;
902
+ border: 1px solid var(--border);
903
+ border-radius: var(--radius);
904
+ background: var(--bg-input);
905
+ color: var(--text-primary);
906
+ font-family: 'SF Mono', 'Fira Code', monospace;
907
+ font-size: 13px;
908
+ outline: none;
909
+ transition: border-color 0.15s;
910
+ }
911
+ .api-tester-url:focus {
912
+ border-color: var(--accent);
913
+ }
914
+ .api-tester-url::placeholder {
915
+ color: var(--text-muted);
916
+ }
917
+ .api-tester-send {
918
+ padding: 8px 20px;
919
+ border: none;
920
+ border-radius: var(--radius);
921
+ background: var(--accent);
922
+ color: white;
923
+ font-weight: 600;
924
+ font-size: 13px;
925
+ font-family: inherit;
926
+ cursor: pointer;
927
+ display: flex;
928
+ align-items: center;
929
+ gap: 6px;
930
+ transition: opacity 0.15s;
931
+ }
932
+ .api-tester-send:hover {
933
+ opacity: 0.9;
934
+ }
935
+ .api-tester-send:disabled {
936
+ opacity: 0.5;
937
+ cursor: not-allowed;
938
+ }
939
+ .api-tester-tabs {
940
+ display: flex;
941
+ gap: 0;
942
+ border-bottom: 1px solid var(--border);
943
+ margin-bottom: 0;
944
+ }
945
+ .api-tester-tab {
946
+ padding: 8px 16px;
947
+ font-size: 12px;
948
+ font-weight: 500;
949
+ color: var(--text-muted);
950
+ border: none;
951
+ background: none;
952
+ cursor: pointer;
953
+ border-bottom: 2px solid transparent;
954
+ font-family: inherit;
955
+ transition: all 0.15s;
956
+ }
957
+ .api-tester-tab:hover {
958
+ color: var(--text-primary);
959
+ }
960
+ .api-tester-tab.active {
961
+ color: var(--accent-light);
962
+ border-bottom-color: var(--accent);
963
+ }
964
+ .api-tester-body-editor {
965
+ width: 100%;
966
+ min-height: 180px;
967
+ padding: 14px;
968
+ border: none;
969
+ outline: none;
970
+ font-family: 'SF Mono', 'Fira Code', monospace;
971
+ font-size: 12px;
972
+ line-height: 1.6;
973
+ background: var(--bg-card);
974
+ color: var(--text-primary);
975
+ resize: vertical;
976
+ tab-size: 2;
977
+ }
978
+ .api-tester-kv-row {
979
+ display: flex;
980
+ gap: 8px;
981
+ align-items: center;
982
+ padding: 6px 14px;
983
+ border-bottom: 1px solid var(--border);
984
+ }
985
+ .api-tester-kv-row input {
986
+ flex: 1;
987
+ padding: 6px 10px;
988
+ border: 1px solid var(--border);
989
+ border-radius: 4px;
990
+ background: var(--bg-input);
991
+ color: var(--text-primary);
992
+ font-size: 12px;
993
+ font-family: 'SF Mono', monospace;
994
+ outline: none;
995
+ }
996
+ .api-tester-kv-row input:focus {
997
+ border-color: var(--accent);
998
+ }
999
+ .api-tester-status {
1000
+ display: inline-flex;
1001
+ align-items: center;
1002
+ padding: 3px 10px;
1003
+ border-radius: 4px;
1004
+ font-size: 12px;
1005
+ font-weight: 600;
1006
+ font-family: 'SF Mono', monospace;
1007
+ }
1008
+ .api-tester-status.s2xx { background: rgba(52,211,153,0.12); color: var(--success); }
1009
+ .api-tester-status.s3xx { background: rgba(96,165,250,0.12); color: #60a5fa; }
1010
+ .api-tester-status.s4xx { background: rgba(251,191,36,0.12); color: var(--warning); }
1011
+ .api-tester-status.s5xx { background: rgba(248,113,113,0.12); color: var(--danger); }
1012
+ .api-tester-status.s0xx { background: rgba(248,113,113,0.12); color: var(--danger); }
1013
+ .api-tester-response-body {
1014
+ padding: 14px;
1015
+ font-family: 'SF Mono', 'Fira Code', monospace;
1016
+ font-size: 12px;
1017
+ line-height: 1.6;
1018
+ white-space: pre-wrap;
1019
+ word-break: break-word;
1020
+ color: var(--text-secondary);
1021
+ max-height: 400px;
1022
+ overflow-y: auto;
1023
+ }
1024
+ .api-tester-saved-item {
1025
+ padding: 10px 14px;
1026
+ cursor: pointer;
1027
+ border-bottom: 1px solid var(--border);
1028
+ transition: background 0.1s;
1029
+ }
1030
+ .api-tester-saved-item:hover {
1031
+ background: var(--bg-input);
1032
+ }
1033
+ .api-tester-saved-item.active {
1034
+ background: var(--accent-glow);
1035
+ border-left: 2px solid var(--accent);
1036
+ }
1037
+
1038
+ .api-tester-mode-tabs {
1039
+ display: inline-flex;
1040
+ gap: 2px;
1041
+ padding: 3px;
1042
+ background: var(--bg-input);
1043
+ border-radius: 8px;
1044
+ margin-bottom: 16px;
1045
+ }
1046
+ .api-tester-mode-tab {
1047
+ padding: 6px 16px;
1048
+ border-radius: 6px;
1049
+ border: none;
1050
+ cursor: pointer;
1051
+ display: flex;
1052
+ align-items: center;
1053
+ gap: 6px;
1054
+ font-size: 13px;
1055
+ font-weight: 400;
1056
+ font-family: inherit;
1057
+ background: transparent;
1058
+ color: var(--text-muted);
1059
+ transition: all 0.15s;
1060
+ }
1061
+ .api-tester-mode-tab.active {
1062
+ background: var(--bg-surface);
1063
+ color: var(--text-primary);
1064
+ font-weight: 600;
1065
+ box-shadow: 0 1px 3px rgba(0,0,0,0.2);
1066
+ }
1067
+ .ws-log {
1068
+ max-height: 350px;
1069
+ overflow-y: auto;
1070
+ font-family: 'SF Mono', 'Fira Code', monospace;
1071
+ font-size: 12px;
1072
+ line-height: 1.6;
1073
+ }
1074
+ .ws-log-entry {
1075
+ padding: 4px 14px;
1076
+ border-bottom: 1px solid var(--border);
1077
+ display: flex;
1078
+ gap: 8px;
1079
+ align-items: flex-start;
1080
+ }
1081
+ .ws-log-entry .dir {
1082
+ font-size: 10px;
1083
+ font-weight: 600;
1084
+ padding: 1px 5px;
1085
+ border-radius: 3px;
1086
+ flex-shrink: 0;
1087
+ margin-top: 2px;
1088
+ }
1089
+ .ws-log-entry .dir.sent {
1090
+ background: rgba(59,130,246,0.12);
1091
+ color: #60a5fa;
1092
+ }
1093
+ .ws-log-entry .dir.received {
1094
+ background: rgba(52,211,153,0.12);
1095
+ color: var(--success);
1096
+ }
1097
+ .ws-log-entry .dir.system {
1098
+ background: rgba(251,191,36,0.12);
1099
+ color: var(--warning);
1100
+ }
1101
+ .ws-log-entry .msg {
1102
+ white-space: pre-wrap;
1103
+ word-break: break-word;
1104
+ color: var(--text-secondary);
1105
+ }
1106
+ .ws-log-entry .ts {
1107
+ font-size: 10px;
1108
+ color: var(--text-muted);
1109
+ margin-left: auto;
1110
+ flex-shrink: 0;
1111
+ }
1112
+ .ws-connected {
1113
+ display: inline-flex;
1114
+ align-items: center;
1115
+ gap: 5px;
1116
+ font-size: 12px;
1117
+ font-weight: 500;
1118
+ }
1119
+ .ws-connected .dot {
1120
+ width: 7px;
1121
+ height: 7px;
1122
+ border-radius: 50%;
1123
+ }
1124
+ .ws-connected .dot.on { background: var(--success); }
1125
+ .ws-connected .dot.off { background: var(--text-muted); }
1126
+
1127
+ /* ── Pipeline/Flow ── */
1128
+ .pipeline {
1129
+ display: flex;
1130
+ align-items: center;
1131
+ gap: 0;
1132
+ margin-bottom: 32px;
1133
+ overflow-x: auto;
1134
+ padding: 8px 0;
1135
+ }
1136
+ .pipeline-step {
1137
+ display: flex;
1138
+ flex-direction: column;
1139
+ align-items: center;
1140
+ gap: 8px;
1141
+ min-width: 100px;
1142
+ padding: 16px;
1143
+ }
1144
+ .pipeline-dot {
1145
+ width: 40px;
1146
+ height: 40px;
1147
+ border-radius: 50%;
1148
+ display: flex;
1149
+ align-items: center;
1150
+ justify-content: center;
1151
+ font-size: 16px;
1152
+ border: 2px solid var(--border);
1153
+ background: var(--bg-card);
1154
+ transition: all 0.3s;
1155
+ }
1156
+ .pipeline-dot.done {
1157
+ border-color: var(--success);
1158
+ background: var(--success-glow);
1159
+ color: var(--success);
1160
+ }
1161
+ .pipeline-dot.current {
1162
+ border-color: var(--accent);
1163
+ background: var(--accent-glow);
1164
+ color: var(--accent-light);
1165
+ box-shadow: 0 0 20px var(--accent-glow);
1166
+ }
1167
+ .pipeline-label {
1168
+ font-size: 11px;
1169
+ font-weight: 500;
1170
+ color: var(--text-muted);
1171
+ text-align: center;
1172
+ }
1173
+ .pipeline-label.done {
1174
+ color: var(--success);
1175
+ }
1176
+ .pipeline-label.current {
1177
+ color: var(--accent-light);
1178
+ }
1179
+ .pipeline-connector {
1180
+ width: 40px;
1181
+ height: 2px;
1182
+ background: var(--border);
1183
+ flex-shrink: 0;
1184
+ margin-top: -24px;
1185
+ }
1186
+ .pipeline-connector.done {
1187
+ background: var(--success);
1188
+ }
1189
+
1190
+ /* ── Toast ── */
1191
+ .toast-container {
1192
+ position: fixed;
1193
+ top: 16px;
1194
+ right: 16px;
1195
+ z-index: 1000;
1196
+ display: flex;
1197
+ flex-direction: column;
1198
+ gap: 8px;
1199
+ }
1200
+ .toast {
1201
+ padding: 12px 16px;
1202
+ border-radius: var(--radius);
1203
+ font-size: 13px;
1204
+ font-weight: 500;
1205
+ background: var(--bg-card);
1206
+ border: 1px solid var(--border);
1207
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
1208
+ animation: slideIn 0.2s ease;
1209
+ max-width: 400px;
1210
+ }
1211
+ .toast.success {
1212
+ border-left: 3px solid var(--success);
1213
+ }
1214
+ .toast.error {
1215
+ border-left: 3px solid var(--error);
1216
+ }
1217
+ .toast.info {
1218
+ border-left: 3px solid var(--info);
1219
+ }
1220
+
1221
+ @keyframes slideIn {
1222
+ from {
1223
+ transform: translateX(100%);
1224
+ opacity: 0;
1225
+ }
1226
+ to {
1227
+ transform: translateX(0);
1228
+ opacity: 1;
1229
+ }
1230
+ }
1231
+
1232
+ /* ── Modal ── */
1233
+ .modal-overlay {
1234
+ position: fixed;
1235
+ inset: 0;
1236
+ background: rgba(0, 0, 0, 0.6);
1237
+ backdrop-filter: blur(4px);
1238
+ display: flex;
1239
+ align-items: center;
1240
+ justify-content: center;
1241
+ z-index: 900;
1242
+ animation: fadeIn 0.15s ease;
1243
+ }
1244
+ .modal {
1245
+ background: var(--bg-card);
1246
+ border: 1px solid var(--border);
1247
+ border-radius: var(--radius-xl);
1248
+ padding: 24px;
1249
+ min-width: 420px;
1250
+ max-width: 560px;
1251
+ max-height: 80vh;
1252
+ overflow-y: auto;
1253
+ box-shadow: 0 24px 64px rgba(0, 0, 0, 0.5);
1254
+ animation: scaleIn 0.15s ease;
1255
+ }
1256
+ .modal-title {
1257
+ font-size: 16px;
1258
+ font-weight: 600;
1259
+ margin-bottom: 16px;
1260
+ }
1261
+ .modal-actions {
1262
+ display: flex;
1263
+ gap: 8px;
1264
+ justify-content: flex-end;
1265
+ margin-top: 20px;
1266
+ }
1267
+
1268
+ @keyframes fadeIn {
1269
+ from {
1270
+ opacity: 0;
1271
+ }
1272
+ to {
1273
+ opacity: 1;
1274
+ }
1275
+ }
1276
+ @keyframes scaleIn {
1277
+ from {
1278
+ transform: scale(0.96);
1279
+ opacity: 0;
1280
+ }
1281
+ to {
1282
+ transform: scale(1);
1283
+ opacity: 1;
1284
+ }
1285
+ }
1286
+
1287
+ /* ── Code block ── */
1288
+ .code-block {
1289
+ background: #11141e;
1290
+ border: 1px solid var(--border);
1291
+ border-radius: var(--radius);
1292
+ padding: 16px;
1293
+ font-family: "SF Mono", "Fira Code", monospace;
1294
+ font-size: 12px;
1295
+ line-height: 1.6;
1296
+ overflow-x: auto;
1297
+ white-space: pre;
1298
+ color: var(--text-secondary);
1299
+ }
1300
+
1301
+ /* ── Checkbox ── */
1302
+ .checkbox-group {
1303
+ display: flex;
1304
+ flex-wrap: wrap;
1305
+ gap: 8px;
1306
+ }
1307
+ .checkbox-item {
1308
+ display: flex;
1309
+ align-items: center;
1310
+ gap: 8px;
1311
+ padding: 8px 14px;
1312
+ background: var(--bg-surface);
1313
+ border: 1px solid var(--border);
1314
+ border-radius: var(--radius);
1315
+ cursor: pointer;
1316
+ font-size: 13px;
1317
+ transition: all var(--transition);
1318
+ }
1319
+ .checkbox-item:hover {
1320
+ border-color: rgba(255, 255, 255, 0.12);
1321
+ }
1322
+ .checkbox-item.checked {
1323
+ border-color: var(--accent);
1324
+ background: var(--accent-glow);
1325
+ color: var(--accent-light);
1326
+ }
1327
+ .checkbox-item input {
1328
+ display: none;
1329
+ }
1330
+
1331
+ /* ── Utilities ── */
1332
+ .flex {
1333
+ display: flex;
1334
+ }
1335
+ .flex-col {
1336
+ flex-direction: column;
1337
+ }
1338
+ .items-center {
1339
+ align-items: center;
1340
+ }
1341
+ .justify-between {
1342
+ justify-content: space-between;
1343
+ }
1344
+ .gap-2 {
1345
+ gap: 8px;
1346
+ }
1347
+ .gap-3 {
1348
+ gap: 12px;
1349
+ }
1350
+ .gap-4 {
1351
+ gap: 16px;
1352
+ }
1353
+ .mb-2 {
1354
+ margin-bottom: 8px;
1355
+ }
1356
+ .mb-3 {
1357
+ margin-bottom: 12px;
1358
+ }
1359
+ .mb-4 {
1360
+ margin-bottom: 16px;
1361
+ }
1362
+ .mb-6 {
1363
+ margin-bottom: 24px;
1364
+ }
1365
+ .mt-4 {
1366
+ margin-top: 16px;
1367
+ }
1368
+ .text-sm {
1369
+ font-size: 12px;
1370
+ }
1371
+ .text-muted {
1372
+ color: var(--text-muted);
1373
+ }
1374
+ .text-secondary {
1375
+ color: var(--text-secondary);
1376
+ }
1377
+ .w-full {
1378
+ width: 100%;
1379
+ }
1380
+
1381
+ /* ── Spinner ── */
1382
+ .spinner {
1383
+ width: 16px;
1384
+ height: 16px;
1385
+ border: 2px solid rgba(255, 255, 255, 0.15);
1386
+ border-top-color: var(--accent-light);
1387
+ border-radius: 50%;
1388
+ animation: spin 0.6s linear infinite;
1389
+ }
1390
+ @keyframes spin {
1391
+ to {
1392
+ transform: rotate(360deg);
1393
+ }
1394
+ }
1395
+ @keyframes pulse {
1396
+ 0%, 100% { opacity: 1; }
1397
+ 50% { opacity: 0.7; }
1398
+ }
1399
+ .spin {
1400
+ animation: spin 0.6s linear infinite;
1401
+ }
1402
+
1403
+ /* ── Env editor ── */
1404
+ .env-editor {
1405
+ width: 100%;
1406
+ min-height: 280px;
1407
+ background: var(--bg-input);
1408
+ border: 1px solid var(--border);
1409
+ border-radius: var(--radius);
1410
+ color: var(--text-primary);
1411
+ font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
1412
+ font-size: 13px;
1413
+ line-height: 1.6;
1414
+ padding: 14px 16px;
1415
+ resize: vertical;
1416
+ outline: none;
1417
+ transition: border-color 0.15s;
1418
+ }
1419
+ .env-editor:focus {
1420
+ border-color: var(--accent);
1421
+ }
1422
+
1423
+ /* ── Splash Screen ── */
1424
+ .splash {
1425
+ position: fixed;
1426
+ inset: 0;
1427
+ z-index: 9999;
1428
+ display: flex;
1429
+ flex-direction: column;
1430
+ align-items: center;
1431
+ justify-content: center;
1432
+ background: var(--bg);
1433
+ }
1434
+ .splash-logo {
1435
+ font-family: monospace;
1436
+ font-size: 10px;
1437
+ line-height: 1.15;
1438
+ letter-spacing: 1px;
1439
+ white-space: pre;
1440
+ background: linear-gradient(135deg, #3daefd, #0079ff, #005eff);
1441
+ -webkit-background-clip: text;
1442
+ -webkit-text-fill-color: transparent;
1443
+ user-select: none;
1444
+ text-align: center;
1445
+ }
1446
+ .splash-tagline {
1447
+ font-family: "Noto Sans", sans-serif;
1448
+ font-size: 16px;
1449
+ font-weight: 400;
1450
+ color: var(--text-secondary);
1451
+ letter-spacing: 0.5px;
1452
+ margin-top: 20px;
1453
+ }
1454
+ .splash-company {
1455
+ position: absolute;
1456
+ bottom: 36px;
1457
+ font-family: "Noto Sans", sans-serif;
1458
+ font-size: 12px;
1459
+ color: var(--text-muted);
1460
+ letter-spacing: 0.3px;
1461
+ }
1462
+ .splash-company span {
1463
+ font-weight: 600;
1464
+ background: linear-gradient(135deg, #3daefd, #005eff);
1465
+ -webkit-background-clip: text;
1466
+ -webkit-text-fill-color: transparent;
1467
+ }
1468
+ .splash-glow {
1469
+ position: absolute;
1470
+ width: 300px;
1471
+ height: 300px;
1472
+ border-radius: 50%;
1473
+ background: radial-gradient(circle, rgba(0,94,255,0.12) 0%, transparent 70%);
1474
+ pointer-events: none;
1475
+ }
1476
+
1477
+ /* ── Passkey Login Screen ── */
1478
+ .passkey-screen {
1479
+ position: fixed;
1480
+ inset: 0;
1481
+ z-index: 9999;
1482
+ display: flex;
1483
+ flex-direction: column;
1484
+ align-items: center;
1485
+ justify-content: center;
1486
+ background: var(--bg);
1487
+ }
1488
+ .passkey-card {
1489
+ display: flex;
1490
+ flex-direction: column;
1491
+ align-items: center;
1492
+ gap: 24px;
1493
+ padding: 48px 40px;
1494
+ background: var(--card);
1495
+ border: 1px solid var(--border);
1496
+ border-radius: 16px;
1497
+ min-width: 340px;
1498
+ }
1499
+ .passkey-card h2 {
1500
+ font-family: "Noto Sans", sans-serif;
1501
+ font-size: 20px;
1502
+ font-weight: 600;
1503
+ color: var(--text);
1504
+ margin: 0;
1505
+ }
1506
+ .passkey-card p {
1507
+ font-family: "Noto Sans", sans-serif;
1508
+ font-size: 13px;
1509
+ color: var(--text-secondary);
1510
+ margin: 0;
1511
+ }
1512
+ .passkey-field {
1513
+ width: 100%;
1514
+ }
1515
+ .passkey-field input {
1516
+ width: 100%;
1517
+ height: 48px;
1518
+ padding: 0 14px;
1519
+ font-size: 16px;
1520
+ font-weight: 500;
1521
+ font-family: "Noto Sans", sans-serif;
1522
+ background: var(--bg);
1523
+ border: 1px solid var(--border);
1524
+ border-radius: 10px;
1525
+ color: var(--text);
1526
+ outline: none;
1527
+ transition: border-color 0.2s;
1528
+ box-sizing: border-box;
1529
+ }
1530
+ .passkey-field input:focus {
1531
+ border-color: #0079ff;
1532
+ box-shadow: 0 0 0 2px rgba(0, 94, 255, 0.15);
1533
+ }
1534
+ .passkey-field input.error {
1535
+ border-color: #ef4444;
1536
+ box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.15);
1537
+ }
1538
+ .passkey-error {
1539
+ font-family: "Noto Sans", sans-serif;
1540
+ font-size: 13px;
1541
+ color: #ef4444;
1542
+ min-height: 18px;
1543
+ }
1544
+ .passkey-btn {
1545
+ width: 100%;
1546
+ padding: 12px;
1547
+ font-family: "Noto Sans", sans-serif;
1548
+ font-size: 14px;
1549
+ font-weight: 600;
1550
+ color: #fff;
1551
+ background: linear-gradient(135deg, #0079ff, #005eff);
1552
+ border: none;
1553
+ border-radius: 10px;
1554
+ cursor: pointer;
1555
+ transition: opacity 0.2s;
1556
+ }
1557
+ .passkey-btn:hover { opacity: 0.9; }
1558
+ .passkey-btn:disabled { opacity: 0.5; cursor: not-allowed; }
1559
+ </style>
1560
+ </head>
1561
+ <body>
1562
+ <div id="root"></div>
1563
+ <script type="importmap">
1564
+ {
1565
+ "imports": {
1566
+ "react": "https://esm.sh/react@18.3.1",
1567
+ "react/jsx-runtime": "https://esm.sh/react@18.3.1/jsx-runtime",
1568
+ "react-dom/client": "https://esm.sh/react-dom@18.3.1/client",
1569
+ "htm": "https://esm.sh/htm@3.1.1",
1570
+ "react-icons/lu": "https://esm.sh/react-icons@4.12.0/lu?external=react",
1571
+ "framer-motion": "https://esm.sh/framer-motion@11.0.3?external=react,react-dom",
1572
+ "socket.io-client": "https://esm.sh/socket.io-client@4.7.4"
1573
+ }
1574
+ }
1575
+ </script>
1576
+ <script type="module" src="/_dev/assets/js/main.js"></script>
1577
+ </body>
1578
+ </html>