levix-bot 2.0.0

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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -0
  3. package/SETUP.md +242 -0
  4. package/app.cjs +337 -0
  5. package/bin/levix.js +7 -0
  6. package/deploy/install.sh +182 -0
  7. package/deploy/levix.service +56 -0
  8. package/deploy/nginx/levix.leviro.net.conf +79 -0
  9. package/package.json +78 -0
  10. package/public/brand/apple-touch-icon.png +0 -0
  11. package/public/brand/banner.jpg +0 -0
  12. package/public/brand/banner.webp +0 -0
  13. package/public/brand/icon-16.png +0 -0
  14. package/public/brand/icon-192.png +0 -0
  15. package/public/brand/icon-32.png +0 -0
  16. package/public/brand/icon-512.png +0 -0
  17. package/public/brand/icon-512.webp +0 -0
  18. package/public/brand/mark.png +0 -0
  19. package/public/brand/mark.svg +16 -0
  20. package/public/brand/mark.webp +0 -0
  21. package/public/brand/wordmark.png +0 -0
  22. package/public/brand/wordmark.webp +0 -0
  23. package/public/dashboard.css +881 -0
  24. package/public/dashboard.js +1057 -0
  25. package/public/logo.svg +16 -0
  26. package/public/qrcode.min.js +1 -0
  27. package/public/socket.io.min.js +7 -0
  28. package/scheduler.cjs +197 -0
  29. package/src/auth/auth-storage.cjs +74 -0
  30. package/src/auth/use-database-auth-state.js +130 -0
  31. package/src/bootstrap/core.js +74 -0
  32. package/src/bootstrap/events.cjs +43 -0
  33. package/src/bootstrap/panel.js +97 -0
  34. package/src/cli.js +153 -0
  35. package/src/commands/autoschedule.cjs +84 -0
  36. package/src/commands/blacklist.cjs +116 -0
  37. package/src/commands/block.cjs +70 -0
  38. package/src/commands/calc.cjs +387 -0
  39. package/src/commands/debt.cjs +175 -0
  40. package/src/commands/deleteschedule.cjs +36 -0
  41. package/src/commands/gemini.cjs +727 -0
  42. package/src/commands/group/add.cjs +136 -0
  43. package/src/commands/group/all.cjs +43 -0
  44. package/src/commands/group/antiSpam.cjs +96 -0
  45. package/src/commands/group/antilink.cjs +167 -0
  46. package/src/commands/group/approveall.cjs +54 -0
  47. package/src/commands/group/clearwarns.cjs +41 -0
  48. package/src/commands/group/deletenote.cjs +33 -0
  49. package/src/commands/group/demote.cjs +62 -0
  50. package/src/commands/group/kick.cjs +85 -0
  51. package/src/commands/group/media.cjs +120 -0
  52. package/src/commands/group/members.cjs +59 -0
  53. package/src/commands/group/note.cjs +32 -0
  54. package/src/commands/group/promote.cjs +60 -0
  55. package/src/commands/group/removeall.cjs +42 -0
  56. package/src/commands/group/rules.cjs +31 -0
  57. package/src/commands/group/save.cjs +36 -0
  58. package/src/commands/group/setname.cjs +41 -0
  59. package/src/commands/group/setpp.cjs +51 -0
  60. package/src/commands/group/setrules.cjs +46 -0
  61. package/src/commands/group/setwarn.cjs +58 -0
  62. package/src/commands/group/tagadmins.cjs +50 -0
  63. package/src/commands/group/warn.cjs +105 -0
  64. package/src/commands/group/warns.cjs +66 -0
  65. package/src/commands/group/welcome.cjs +109 -0
  66. package/src/commands/group.cjs +160 -0
  67. package/src/commands/help.cjs +230 -0
  68. package/src/commands/listschedules.cjs +47 -0
  69. package/src/commands/loop.cjs +163 -0
  70. package/src/commands/memory.cjs +201 -0
  71. package/src/commands/mod.cjs +155 -0
  72. package/src/commands/notes.cjs +28 -0
  73. package/src/commands/perm.cjs +236 -0
  74. package/src/commands/ping.cjs +17 -0
  75. package/src/commands/poll.cjs +80 -0
  76. package/src/commands/prayer.cjs +64 -0
  77. package/src/commands/qr.cjs +39 -0
  78. package/src/commands/rand.cjs +130 -0
  79. package/src/commands/restart.cjs +25 -0
  80. package/src/commands/schedule.cjs +86 -0
  81. package/src/commands/score.cjs +130 -0
  82. package/src/commands/setprefix.cjs +40 -0
  83. package/src/commands/shortlink.cjs +74 -0
  84. package/src/commands/shutdown.cjs +45 -0
  85. package/src/commands/status.cjs +75 -0
  86. package/src/commands/stt.cjs +150 -0
  87. package/src/commands/todo.cjs +88 -0
  88. package/src/commands/tts.cjs +170 -0
  89. package/src/commands/unblock.cjs +57 -0
  90. package/src/commands/weather.cjs +79 -0
  91. package/src/config/ai-persona.md +17 -0
  92. package/src/config/baileys.config.js +51 -0
  93. package/src/config/brand.cjs +36 -0
  94. package/src/config/brand.esm.js +17 -0
  95. package/src/config/constants.js +16 -0
  96. package/src/config/defaults.cjs +91 -0
  97. package/src/config/lock.cjs +110 -0
  98. package/src/config/paths.cjs +90 -0
  99. package/src/config/runtime-config.cjs +219 -0
  100. package/src/config/secrets.cjs +130 -0
  101. package/src/config/settings.cjs +386 -0
  102. package/src/core/connection.js +199 -0
  103. package/src/core/events.js +61 -0
  104. package/src/core/socket.js +64 -0
  105. package/src/db/db.cjs +282 -0
  106. package/src/db/store.cjs +837 -0
  107. package/src/db/store.esm.js +17 -0
  108. package/src/domain/caddy.js +120 -0
  109. package/src/domain/command.js +380 -0
  110. package/src/domain/detect.js +173 -0
  111. package/src/domain/nginx.js +138 -0
  112. package/src/domain/system.js +148 -0
  113. package/src/handlers/command.handler.js +414 -0
  114. package/src/handlers/group.handler.js +73 -0
  115. package/src/handlers/message.handler.js +149 -0
  116. package/src/index.js +217 -0
  117. package/src/middleware/antispam.middleware.js +68 -0
  118. package/src/middleware/blacklist.middleware.js +29 -0
  119. package/src/middleware/forward-tracking.middleware.js +49 -0
  120. package/src/middleware/permissions.middleware.js +114 -0
  121. package/src/routes/dashboard.api.esm.js +779 -0
  122. package/src/services/aiAgent.cjs +317 -0
  123. package/src/services/aiTools.cjs +635 -0
  124. package/src/utils/datetime.cjs +61 -0
  125. package/src/utils/lid-helper.esm.js +143 -0
  126. package/src/utils/logger.cjs +72 -0
  127. package/src/utils/memory.cjs +385 -0
  128. package/src/utils/normalizeJid.cjs +65 -0
  129. package/src/utils/normalizeJid.esm.js +37 -0
  130. package/src/utils/openBrowser.cjs +129 -0
  131. package/src/utils/permissions.cjs +154 -0
  132. package/src/utils/permissions.esm.js +375 -0
  133. package/src/utils/recentMessageCache.esm.js +70 -0
  134. package/src/utils/requestOrigin.cjs +102 -0
  135. package/src/utils/sendBotMessage.cjs +43 -0
  136. package/src/utils/sendBotMessage.esm.js +139 -0
  137. package/src/utils/statusMessage.cjs +179 -0
  138. package/src/utils/storage-hub.cjs +49 -0
  139. package/src/utils/storage-hub.esm.js +24 -0
  140. package/src/utils/storage.cjs +80 -0
  141. package/src/utils/storage.esm.js +88 -0
  142. package/src/utils/textDecode.cjs +197 -0
  143. package/src/utils/thumbnail.cjs +591 -0
  144. package/src/utils/typing.esm.js +102 -0
  145. package/views/dashboard.ejs +329 -0
  146. package/views/login.ejs +105 -0
  147. package/views/qr.ejs +108 -0
  148. package/views/setup.ejs +122 -0
@@ -0,0 +1,881 @@
1
+ /* Levix dashboard — one stylesheet, no build step, no CDN.
2
+ Dark by default (the logo lives on navy); the toggle in the header stores a
3
+ choice in localStorage and stamps data-theme on <html>. */
4
+
5
+ :root {
6
+ --blue: #0066f5;
7
+ --blue-bright: #3d8bff;
8
+ --cyan: #00d4ff;
9
+ --grad: linear-gradient(135deg, var(--blue) 0%, var(--blue-bright) 55%, var(--cyan) 100%);
10
+
11
+ --bg: #04070e;
12
+ --bg-soft: #070c17;
13
+ --panel: #0a101e;
14
+ --panel-hover: #111a2e;
15
+ --line: #1c2a48;
16
+ --line-soft: #16213a;
17
+
18
+ --text: #eaf1ff;
19
+ --muted: #8ea3c9;
20
+ --faint: #5f739a;
21
+
22
+ --ok: #22c55e;
23
+ --warn: #f59e0b;
24
+ --danger: #ef4444;
25
+ --info: var(--blue-bright);
26
+
27
+ --radius: 14px;
28
+ --radius-sm: 9px;
29
+ --shadow: 0 14px 36px rgba(0, 0, 0, 0.24);
30
+ --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
31
+ }
32
+
33
+ :root[data-theme="light"] {
34
+ color-scheme: light;
35
+ --bg: #f5f7fc;
36
+ --bg-soft: #ffffff;
37
+ --panel: #ffffff;
38
+ --panel-hover: #eef3ff;
39
+ --line: #dbe3f2;
40
+ --line-soft: #e8eefb;
41
+
42
+ --text: #0d1730;
43
+ --muted: #566484;
44
+ --faint: #8492ad;
45
+ --shadow: 0 10px 28px rgba(16, 34, 74, 0.07);
46
+ }
47
+
48
+ * {
49
+ margin: 0;
50
+ padding: 0;
51
+ box-sizing: border-box;
52
+ }
53
+
54
+ html,
55
+ body {
56
+ height: 100%;
57
+ }
58
+
59
+ html {
60
+ color-scheme: dark;
61
+ }
62
+
63
+ body {
64
+ background: var(--bg);
65
+ color: var(--text);
66
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Noto Sans Arabic",
67
+ Tahoma, sans-serif;
68
+ font-size: 15px;
69
+ line-height: 1.55;
70
+ -webkit-font-smoothing: antialiased;
71
+ }
72
+
73
+ ::selection {
74
+ background: color-mix(in srgb, var(--cyan) 28%, transparent);
75
+ color: var(--text);
76
+ }
77
+
78
+ :focus-visible {
79
+ outline: 2px solid var(--cyan);
80
+ outline-offset: 3px;
81
+ }
82
+
83
+ a {
84
+ color: var(--blue-bright);
85
+ text-decoration: none;
86
+ }
87
+
88
+ code,
89
+ kbd {
90
+ font-family: var(--mono);
91
+ font-size: 0.86em;
92
+ background: var(--line-soft);
93
+ border: 1px solid var(--line);
94
+ border-radius: 6px;
95
+ padding: 1px 6px;
96
+ }
97
+
98
+ /* --- shell ------------------------------------------------------------- */
99
+
100
+ .shell {
101
+ display: grid;
102
+ grid-template-columns: 248px 1fr;
103
+ min-height: 100vh;
104
+ }
105
+
106
+ .sidebar {
107
+ background: var(--bg-soft);
108
+ border-right: 1px solid var(--line);
109
+ display: flex;
110
+ flex-direction: column;
111
+ position: sticky;
112
+ top: 0;
113
+ height: 100vh;
114
+ overflow-y: auto;
115
+ }
116
+
117
+ .brand {
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 11px;
121
+ margin: 0 10px;
122
+ padding: 20px 8px 17px;
123
+ border-bottom: 1px solid var(--line-soft);
124
+ }
125
+
126
+ .brand img {
127
+ width: 40px;
128
+ height: 40px;
129
+ }
130
+
131
+ .brand-name {
132
+ font-size: 1.15rem;
133
+ font-weight: 700;
134
+ letter-spacing: -0.01em;
135
+ }
136
+
137
+ .brand-tag {
138
+ font-size: 0.72rem;
139
+ color: var(--faint);
140
+ text-transform: uppercase;
141
+ letter-spacing: 0.09em;
142
+ }
143
+
144
+ .brand-lockup {
145
+ display: inline-flex;
146
+ align-items: center;
147
+ justify-content: center;
148
+ gap: 11px;
149
+ color: var(--text);
150
+ }
151
+
152
+ .brand-lockup img {
153
+ width: 46px;
154
+ height: 46px;
155
+ }
156
+
157
+ .brand-lockup strong {
158
+ font-size: 1.65rem;
159
+ font-weight: 750;
160
+ letter-spacing: -0.04em;
161
+ }
162
+
163
+ .nav {
164
+ padding: 6px 10px;
165
+ display: flex;
166
+ flex-direction: column;
167
+ gap: 2px;
168
+ }
169
+
170
+ .nav-label {
171
+ font-size: 0.68rem;
172
+ text-transform: uppercase;
173
+ letter-spacing: 0.1em;
174
+ color: var(--faint);
175
+ padding: 14px 10px 6px;
176
+ }
177
+
178
+ .nav a {
179
+ display: flex;
180
+ align-items: center;
181
+ gap: 10px;
182
+ padding: 9px 11px;
183
+ border-radius: var(--radius-sm);
184
+ color: var(--muted);
185
+ font-size: 0.92rem;
186
+ font-weight: 500;
187
+ transition: background 0.15s, color 0.15s;
188
+ }
189
+
190
+ .nav a:hover {
191
+ background: var(--panel-hover);
192
+ color: var(--text);
193
+ }
194
+
195
+ .nav a.active {
196
+ background: var(--panel-hover);
197
+ color: var(--text);
198
+ box-shadow: inset 2px 0 0 var(--blue-bright);
199
+ }
200
+
201
+ .nav svg {
202
+ width: 17px;
203
+ height: 17px;
204
+ flex: none;
205
+ opacity: 0.85;
206
+ }
207
+
208
+ .sidebar-foot {
209
+ margin-top: auto;
210
+ padding: 16px 18px;
211
+ border-top: 1px solid var(--line);
212
+ font-size: 0.76rem;
213
+ color: var(--faint);
214
+ }
215
+
216
+ .main {
217
+ display: flex;
218
+ flex-direction: column;
219
+ min-width: 0;
220
+ }
221
+
222
+ .topbar {
223
+ display: flex;
224
+ align-items: center;
225
+ gap: 12px;
226
+ padding: 14px 26px;
227
+ border-bottom: 1px solid var(--line);
228
+ background: color-mix(in srgb, var(--bg) 85%, transparent);
229
+ backdrop-filter: blur(8px);
230
+ position: sticky;
231
+ top: 0;
232
+ z-index: 20;
233
+ }
234
+
235
+ .topbar h1 {
236
+ font-size: 1.08rem;
237
+ font-weight: 650;
238
+ letter-spacing: -0.01em;
239
+ }
240
+
241
+ .topbar .spacer {
242
+ flex: 1;
243
+ }
244
+
245
+ .content {
246
+ padding: 30px;
247
+ max-width: 1240px;
248
+ width: 100%;
249
+ margin: 0 auto;
250
+ }
251
+
252
+ .view {
253
+ display: none;
254
+ }
255
+
256
+ .view.active {
257
+ display: block;
258
+ animation: fade 0.18s ease;
259
+ }
260
+
261
+ @keyframes fade {
262
+ from {
263
+ opacity: 0;
264
+ transform: translateY(4px);
265
+ }
266
+ }
267
+
268
+ /* --- pieces ------------------------------------------------------------ */
269
+
270
+ .pill {
271
+ display: inline-flex;
272
+ align-items: center;
273
+ gap: 7px;
274
+ padding: 5px 12px;
275
+ border-radius: 999px;
276
+ font-size: 0.8rem;
277
+ font-weight: 600;
278
+ border: 1px solid var(--line);
279
+ background: var(--panel);
280
+ color: var(--muted);
281
+ }
282
+
283
+ .pill .dot {
284
+ width: 7px;
285
+ height: 7px;
286
+ border-radius: 50%;
287
+ background: var(--faint);
288
+ }
289
+
290
+ .pill.on {
291
+ color: var(--ok);
292
+ border-color: color-mix(in srgb, var(--ok) 40%, transparent);
293
+ }
294
+ .pill.on .dot {
295
+ background: var(--ok);
296
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 22%, transparent);
297
+ }
298
+ .pill.off {
299
+ color: var(--danger);
300
+ border-color: color-mix(in srgb, var(--danger) 40%, transparent);
301
+ }
302
+ .pill.off .dot {
303
+ background: var(--danger);
304
+ }
305
+
306
+ .btn {
307
+ display: inline-flex;
308
+ align-items: center;
309
+ justify-content: center;
310
+ gap: 7px;
311
+ padding: 8px 15px;
312
+ border: 1px solid var(--line);
313
+ border-radius: var(--radius-sm);
314
+ background: var(--panel);
315
+ color: var(--text);
316
+ font: inherit;
317
+ font-size: 0.88rem;
318
+ font-weight: 550;
319
+ cursor: pointer;
320
+ transition: background 0.15s, border-color 0.15s, opacity 0.15s;
321
+ }
322
+
323
+ .btn:hover {
324
+ background: var(--panel-hover);
325
+ border-color: var(--blue-bright);
326
+ }
327
+
328
+ .btn:disabled {
329
+ opacity: 0.5;
330
+ cursor: not-allowed;
331
+ }
332
+
333
+ .btn-primary {
334
+ background: var(--grad);
335
+ border-color: transparent;
336
+ color: #fff;
337
+ }
338
+
339
+ .btn-primary:hover {
340
+ filter: brightness(1.08);
341
+ border-color: transparent;
342
+ }
343
+
344
+ .btn-danger {
345
+ color: var(--danger);
346
+ border-color: color-mix(in srgb, var(--danger) 45%, transparent);
347
+ }
348
+
349
+ .btn-danger:hover {
350
+ background: color-mix(in srgb, var(--danger) 12%, transparent);
351
+ border-color: var(--danger);
352
+ }
353
+
354
+ .btn-sm {
355
+ padding: 5px 11px;
356
+ font-size: 0.8rem;
357
+ }
358
+
359
+ .btn-icon {
360
+ padding: 8px;
361
+ width: 34px;
362
+ }
363
+
364
+ .card {
365
+ background: var(--panel);
366
+ border: 1px solid var(--line);
367
+ border-radius: var(--radius);
368
+ padding: 18px;
369
+ box-shadow: none;
370
+ }
371
+
372
+ .card + .card {
373
+ margin-top: 16px;
374
+ }
375
+
376
+ .card-head {
377
+ display: flex;
378
+ align-items: center;
379
+ gap: 10px;
380
+ margin-bottom: 14px;
381
+ }
382
+
383
+ .card-head h2 {
384
+ font-size: 0.98rem;
385
+ font-weight: 650;
386
+ }
387
+
388
+ .card-head .spacer {
389
+ flex: 1;
390
+ }
391
+
392
+ .hint {
393
+ color: var(--muted);
394
+ font-size: 0.84rem;
395
+ }
396
+
397
+ .section-head {
398
+ margin-bottom: 18px;
399
+ padding-inline-start: 13px;
400
+ border-inline-start: 2px solid var(--blue-bright);
401
+ }
402
+
403
+ .section-head h2 {
404
+ font-size: 1.35rem;
405
+ font-weight: 700;
406
+ letter-spacing: -0.02em;
407
+ }
408
+
409
+ .section-head p {
410
+ color: var(--muted);
411
+ font-size: 0.9rem;
412
+ margin-top: 3px;
413
+ }
414
+
415
+ .stats {
416
+ display: grid;
417
+ grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
418
+ gap: 14px;
419
+ margin-bottom: 18px;
420
+ }
421
+
422
+ .stat {
423
+ background: var(--panel);
424
+ border: 1px solid var(--line);
425
+ border-radius: var(--radius);
426
+ padding: 15px 16px;
427
+ }
428
+
429
+ .stat .label {
430
+ font-size: 0.72rem;
431
+ text-transform: uppercase;
432
+ letter-spacing: 0.09em;
433
+ color: var(--faint);
434
+ }
435
+
436
+ .stat .value {
437
+ font-size: 1.7rem;
438
+ font-weight: 700;
439
+ letter-spacing: -0.02em;
440
+ margin-top: 3px;
441
+ }
442
+
443
+ .grid-2 {
444
+ display: grid;
445
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
446
+ gap: 16px;
447
+ }
448
+
449
+ /* --- forms ------------------------------------------------------------- */
450
+
451
+ label.field {
452
+ display: block;
453
+ margin-bottom: 14px;
454
+ }
455
+
456
+ label.field > span {
457
+ display: block;
458
+ font-size: 0.84rem;
459
+ font-weight: 550;
460
+ margin-bottom: 5px;
461
+ }
462
+
463
+ input[type="text"],
464
+ input[type="password"],
465
+ input[type="number"],
466
+ select,
467
+ textarea {
468
+ width: 100%;
469
+ padding: 9px 11px;
470
+ background: var(--bg-soft);
471
+ border: 1px solid var(--line);
472
+ border-radius: var(--radius-sm);
473
+ color: var(--text);
474
+ font: inherit;
475
+ font-size: 0.9rem;
476
+ transition: border-color 0.15s;
477
+ }
478
+
479
+ input:focus,
480
+ select:focus,
481
+ textarea:focus {
482
+ outline: none;
483
+ border-color: var(--blue-bright);
484
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--blue-bright) 14%, transparent);
485
+ }
486
+
487
+ textarea {
488
+ min-height: 220px;
489
+ font-family: var(--mono);
490
+ font-size: 0.85rem;
491
+ line-height: 1.6;
492
+ resize: vertical;
493
+ }
494
+
495
+ select {
496
+ cursor: pointer;
497
+ }
498
+
499
+ .switch {
500
+ display: inline-flex;
501
+ align-items: center;
502
+ gap: 9px;
503
+ cursor: pointer;
504
+ user-select: none;
505
+ }
506
+
507
+ .switch input {
508
+ position: absolute;
509
+ opacity: 0;
510
+ width: 0;
511
+ height: 0;
512
+ }
513
+
514
+ .switch .track {
515
+ width: 38px;
516
+ height: 21px;
517
+ border-radius: 999px;
518
+ background: var(--line);
519
+ position: relative;
520
+ transition: background 0.18s;
521
+ flex: none;
522
+ }
523
+
524
+ .switch .track::after {
525
+ content: "";
526
+ position: absolute;
527
+ top: 3px;
528
+ left: 3px;
529
+ width: 15px;
530
+ height: 15px;
531
+ border-radius: 50%;
532
+ background: #fff;
533
+ transition: transform 0.18s;
534
+ }
535
+
536
+ .switch input:checked + .track {
537
+ background: var(--blue-bright);
538
+ }
539
+
540
+ .switch input:checked + .track::after {
541
+ transform: translateX(17px);
542
+ }
543
+
544
+ .inline {
545
+ display: flex;
546
+ gap: 9px;
547
+ align-items: center;
548
+ flex-wrap: wrap;
549
+ }
550
+
551
+ .source {
552
+ font-size: 0.68rem;
553
+ text-transform: uppercase;
554
+ letter-spacing: 0.07em;
555
+ padding: 2px 7px;
556
+ border-radius: 999px;
557
+ border: 1px solid var(--line);
558
+ color: var(--faint);
559
+ }
560
+
561
+ .source.dashboard {
562
+ color: var(--blue-bright);
563
+ border-color: color-mix(in srgb, var(--blue-bright) 40%, transparent);
564
+ }
565
+
566
+ /* --- tables ------------------------------------------------------------ */
567
+
568
+ .table-wrap {
569
+ overflow-x: auto;
570
+ border: 1px solid var(--line);
571
+ border-radius: var(--radius);
572
+ }
573
+
574
+ table {
575
+ width: 100%;
576
+ border-collapse: collapse;
577
+ font-size: 0.875rem;
578
+ }
579
+
580
+ th,
581
+ td {
582
+ text-align: left;
583
+ padding: 10px 13px;
584
+ border-bottom: 1px solid var(--line-soft);
585
+ white-space: nowrap;
586
+ }
587
+
588
+ th {
589
+ background: var(--bg-soft);
590
+ color: var(--muted);
591
+ font-weight: 600;
592
+ font-size: 0.76rem;
593
+ text-transform: uppercase;
594
+ letter-spacing: 0.07em;
595
+ position: sticky;
596
+ top: 0;
597
+ }
598
+
599
+ tbody tr:last-child td {
600
+ border-bottom: none;
601
+ }
602
+
603
+ tbody tr:hover {
604
+ background: var(--panel-hover);
605
+ }
606
+
607
+ td.wrap {
608
+ white-space: normal;
609
+ min-width: 220px;
610
+ }
611
+
612
+ .tag {
613
+ display: inline-block;
614
+ padding: 2px 8px;
615
+ border-radius: 999px;
616
+ font-size: 0.74rem;
617
+ font-weight: 600;
618
+ border: 1px solid var(--line);
619
+ color: var(--muted);
620
+ }
621
+
622
+ .tag.ok {
623
+ color: var(--ok);
624
+ border-color: color-mix(in srgb, var(--ok) 40%, transparent);
625
+ }
626
+ .tag.warn {
627
+ color: var(--warn);
628
+ border-color: color-mix(in srgb, var(--warn) 40%, transparent);
629
+ }
630
+ .tag.danger {
631
+ color: var(--danger);
632
+ border-color: color-mix(in srgb, var(--danger) 40%, transparent);
633
+ }
634
+ .tag.info {
635
+ color: var(--blue-bright);
636
+ border-color: color-mix(in srgb, var(--blue-bright) 40%, transparent);
637
+ }
638
+
639
+ .empty {
640
+ padding: 34px;
641
+ text-align: center;
642
+ color: var(--faint);
643
+ font-size: 0.9rem;
644
+ }
645
+
646
+ .loading {
647
+ padding: 26px;
648
+ text-align: center;
649
+ color: var(--faint);
650
+ font-size: 0.88rem;
651
+ }
652
+
653
+ /* --- commands ---------------------------------------------------------- */
654
+
655
+ .toolbar {
656
+ display: flex;
657
+ gap: 10px;
658
+ align-items: center;
659
+ flex-wrap: wrap;
660
+ margin-bottom: 14px;
661
+ }
662
+
663
+ .toolbar input[type="text"] {
664
+ max-width: 260px;
665
+ }
666
+
667
+ .toolbar select {
668
+ width: auto;
669
+ }
670
+
671
+ .cmd-name {
672
+ font-family: var(--mono);
673
+ font-weight: 600;
674
+ }
675
+
676
+ .alias-input {
677
+ min-width: 190px;
678
+ font-family: var(--mono);
679
+ font-size: 0.82rem;
680
+ }
681
+
682
+ .locked {
683
+ color: var(--faint);
684
+ font-size: 0.78rem;
685
+ display: inline-flex;
686
+ align-items: center;
687
+ gap: 5px;
688
+ }
689
+
690
+ /* --- groups ------------------------------------------------------------ */
691
+
692
+ .group-card summary {
693
+ cursor: pointer;
694
+ display: flex;
695
+ align-items: center;
696
+ gap: 10px;
697
+ list-style: none;
698
+ font-weight: 600;
699
+ }
700
+
701
+ .group-card summary::-webkit-details-marker {
702
+ display: none;
703
+ }
704
+
705
+ .group-card summary .chev {
706
+ margin-left: auto;
707
+ color: var(--faint);
708
+ transition: transform 0.15s;
709
+ }
710
+
711
+ .group-card[open] summary .chev {
712
+ transform: rotate(90deg);
713
+ }
714
+
715
+ .group-body {
716
+ margin-top: 16px;
717
+ padding-top: 16px;
718
+ border-top: 1px solid var(--line);
719
+ display: grid;
720
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
721
+ gap: 18px;
722
+ }
723
+
724
+ .group-body h3 {
725
+ font-size: 0.8rem;
726
+ text-transform: uppercase;
727
+ letter-spacing: 0.08em;
728
+ color: var(--faint);
729
+ margin-bottom: 9px;
730
+ }
731
+
732
+ /* --- QR ---------------------------------------------------------------- */
733
+
734
+ .qr-box {
735
+ display: flex;
736
+ flex-direction: column;
737
+ align-items: center;
738
+ gap: 14px;
739
+ padding: 22px;
740
+ text-align: center;
741
+ }
742
+
743
+ .qr-frame {
744
+ background: #fff;
745
+ padding: 14px;
746
+ border-radius: var(--radius);
747
+ line-height: 0;
748
+ min-width: 268px;
749
+ min-height: 268px;
750
+ display: flex;
751
+ align-items: center;
752
+ justify-content: center;
753
+ }
754
+
755
+ .qr-frame canvas,
756
+ .qr-frame img {
757
+ display: block;
758
+ }
759
+
760
+ .steps {
761
+ color: var(--muted);
762
+ font-size: 0.88rem;
763
+ text-align: left;
764
+ max-width: 380px;
765
+ margin: 0 auto;
766
+ }
767
+
768
+ .steps li {
769
+ margin: 5px 0 5px 18px;
770
+ }
771
+
772
+ /* --- toasts ------------------------------------------------------------ */
773
+
774
+ .toasts {
775
+ position: fixed;
776
+ right: 20px;
777
+ bottom: 20px;
778
+ display: flex;
779
+ flex-direction: column;
780
+ gap: 9px;
781
+ z-index: 100;
782
+ }
783
+
784
+ .toast {
785
+ background: var(--panel);
786
+ border: 1px solid var(--line);
787
+ border-left: 3px solid var(--blue-bright);
788
+ border-radius: var(--radius-sm);
789
+ padding: 11px 15px;
790
+ font-size: 0.88rem;
791
+ box-shadow: var(--shadow);
792
+ max-width: 340px;
793
+ animation: slide 0.2s ease;
794
+ }
795
+
796
+ .toast.ok {
797
+ border-left-color: var(--ok);
798
+ }
799
+ .toast.error {
800
+ border-left-color: var(--danger);
801
+ }
802
+
803
+ @keyframes slide {
804
+ from {
805
+ opacity: 0;
806
+ transform: translateX(14px);
807
+ }
808
+ }
809
+
810
+ /* --- responsive -------------------------------------------------------- */
811
+
812
+ .menu-btn {
813
+ display: none;
814
+ }
815
+
816
+ .sidebar-backdrop {
817
+ display: none;
818
+ position: fixed;
819
+ inset: 0;
820
+ z-index: 50;
821
+ border: 0;
822
+ background: rgba(2, 5, 12, 0.64);
823
+ backdrop-filter: blur(2px);
824
+ }
825
+
826
+ @media (max-width: 900px) {
827
+ .shell {
828
+ grid-template-columns: 1fr;
829
+ }
830
+
831
+ .sidebar {
832
+ position: fixed;
833
+ inset: 0 auto 0 0;
834
+ width: 248px;
835
+ z-index: 60;
836
+ transform: translateX(-100%);
837
+ transition: transform 0.2s;
838
+ }
839
+
840
+ .sidebar.open {
841
+ transform: none;
842
+ }
843
+
844
+ .sidebar-backdrop.open {
845
+ display: block;
846
+ }
847
+
848
+ .menu-btn {
849
+ display: inline-flex;
850
+ }
851
+
852
+ .content {
853
+ padding: 18px 14px;
854
+ }
855
+
856
+ .topbar {
857
+ padding: 12px 14px;
858
+ }
859
+ }
860
+
861
+ @media (max-width: 600px) {
862
+ .topbar {
863
+ gap: 8px;
864
+ }
865
+
866
+ .topbar h1 {
867
+ display: none;
868
+ }
869
+
870
+ .topbar .pill {
871
+ padding-inline: 9px;
872
+ }
873
+
874
+ .stats {
875
+ grid-template-columns: repeat(2, minmax(0, 1fr));
876
+ }
877
+
878
+ .grid-2 {
879
+ grid-template-columns: 1fr;
880
+ }
881
+ }