millas 0.2.12-beta → 0.2.12-beta-2

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 (116) hide show
  1. package/package.json +3 -16
  2. package/src/admin/ActivityLog.js +153 -52
  3. package/src/admin/Admin.js +400 -167
  4. package/src/admin/AdminAuth.js +213 -98
  5. package/src/admin/FormGenerator.js +372 -0
  6. package/src/admin/HookRegistry.js +256 -0
  7. package/src/admin/QueryEngine.js +263 -0
  8. package/src/admin/ViewContext.js +309 -0
  9. package/src/admin/WidgetRegistry.js +406 -0
  10. package/src/admin/index.js +17 -0
  11. package/src/admin/resources/AdminResource.js +383 -97
  12. package/src/admin/static/admin.css +1341 -0
  13. package/src/admin/static/date-picker.css +157 -0
  14. package/src/admin/static/date-picker.js +316 -0
  15. package/src/admin/static/json-editor.css +649 -0
  16. package/src/admin/static/json-editor.js +1429 -0
  17. package/src/admin/static/ui.js +1044 -0
  18. package/src/admin/views/layouts/base.njk +65 -1013
  19. package/src/admin/views/pages/detail.njk +40 -16
  20. package/src/admin/views/pages/form.njk +47 -599
  21. package/src/admin/views/pages/list.njk +145 -62
  22. package/src/admin/views/partials/form-field.njk +53 -0
  23. package/src/admin/views/partials/form-footer.njk +28 -0
  24. package/src/admin/views/partials/form-readonly.njk +114 -0
  25. package/src/admin/views/partials/form-scripts.njk +476 -0
  26. package/src/admin/views/partials/form-widget.njk +296 -0
  27. package/src/admin/views/partials/json-dialog.njk +80 -0
  28. package/src/admin/views/partials/json-editor.njk +37 -0
  29. package/src/admin.zip +0 -0
  30. package/src/auth/Auth.js +31 -10
  31. package/src/auth/AuthController.js +3 -1
  32. package/src/auth/AuthUser.js +119 -0
  33. package/src/cli.js +4 -2
  34. package/src/commands/createsuperuser.js +254 -0
  35. package/src/commands/lang.js +589 -0
  36. package/src/commands/migrate.js +154 -81
  37. package/src/commands/serve.js +82 -110
  38. package/src/container/AppInitializer.js +215 -0
  39. package/src/container/Application.js +278 -253
  40. package/src/container/HttpServer.js +156 -0
  41. package/src/container/MillasApp.js +29 -279
  42. package/src/container/MillasConfig.js +192 -0
  43. package/src/core/admin.js +5 -0
  44. package/src/core/auth.js +9 -0
  45. package/src/core/db.js +9 -0
  46. package/src/core/foundation.js +59 -0
  47. package/src/core/http.js +11 -0
  48. package/src/core/lang.js +1 -0
  49. package/src/core/mail.js +6 -0
  50. package/src/core/queue.js +7 -0
  51. package/src/core/validation.js +29 -0
  52. package/src/facades/Admin.js +1 -1
  53. package/src/facades/Auth.js +22 -39
  54. package/src/facades/Cache.js +21 -10
  55. package/src/facades/Database.js +1 -1
  56. package/src/facades/Events.js +18 -17
  57. package/src/facades/Facade.js +197 -0
  58. package/src/facades/Http.js +42 -45
  59. package/src/facades/Log.js +25 -49
  60. package/src/facades/Mail.js +27 -32
  61. package/src/facades/Queue.js +22 -15
  62. package/src/facades/Storage.js +18 -10
  63. package/src/facades/Url.js +53 -0
  64. package/src/http/HttpClient.js +673 -0
  65. package/src/http/ResponseDispatcher.js +18 -111
  66. package/src/http/UrlGenerator.js +375 -0
  67. package/src/http/WelcomePage.js +273 -0
  68. package/src/http/adapters/ExpressAdapter.js +315 -0
  69. package/src/http/adapters/HttpAdapter.js +168 -0
  70. package/src/http/adapters/index.js +9 -0
  71. package/src/i18n/I18nServiceProvider.js +91 -0
  72. package/src/i18n/Translator.js +635 -0
  73. package/src/i18n/defaults.js +122 -0
  74. package/src/i18n/index.js +164 -0
  75. package/src/i18n/locales/en.js +55 -0
  76. package/src/i18n/locales/sw.js +48 -0
  77. package/src/index.js +5 -144
  78. package/src/logger/formatters/PrettyFormatter.js +103 -57
  79. package/src/logger/internal.js +2 -2
  80. package/src/logger/patchConsole.js +91 -81
  81. package/src/middleware/MiddlewareRegistry.js +62 -82
  82. package/src/migrations/system/0001_users.js +21 -0
  83. package/src/migrations/system/0002_admin_log.js +25 -0
  84. package/src/migrations/system/0003_sessions.js +23 -0
  85. package/src/orm/fields/index.js +210 -188
  86. package/src/orm/migration/DefaultValueParser.js +325 -0
  87. package/src/orm/migration/InteractiveResolver.js +191 -0
  88. package/src/orm/migration/Makemigrations.js +312 -0
  89. package/src/orm/migration/MigrationGraph.js +227 -0
  90. package/src/orm/migration/MigrationRunner.js +202 -108
  91. package/src/orm/migration/MigrationWriter.js +463 -0
  92. package/src/orm/migration/ModelInspector.js +412 -344
  93. package/src/orm/migration/ModelScanner.js +225 -0
  94. package/src/orm/migration/ProjectState.js +213 -0
  95. package/src/orm/migration/RenameDetector.js +175 -0
  96. package/src/orm/migration/SchemaBuilder.js +8 -81
  97. package/src/orm/migration/operations/base.js +57 -0
  98. package/src/orm/migration/operations/column.js +191 -0
  99. package/src/orm/migration/operations/fields.js +252 -0
  100. package/src/orm/migration/operations/index.js +55 -0
  101. package/src/orm/migration/operations/models.js +152 -0
  102. package/src/orm/migration/operations/registry.js +131 -0
  103. package/src/orm/migration/operations/special.js +51 -0
  104. package/src/orm/migration/utils.js +208 -0
  105. package/src/orm/model/Model.js +81 -13
  106. package/src/providers/AdminServiceProvider.js +66 -9
  107. package/src/providers/AuthServiceProvider.js +46 -7
  108. package/src/providers/CacheStorageServiceProvider.js +5 -3
  109. package/src/providers/DatabaseServiceProvider.js +3 -2
  110. package/src/providers/EventServiceProvider.js +2 -1
  111. package/src/providers/LogServiceProvider.js +7 -3
  112. package/src/providers/MailServiceProvider.js +4 -3
  113. package/src/providers/QueueServiceProvider.js +4 -3
  114. package/src/router/Router.js +119 -152
  115. package/src/scaffold/templates.js +83 -26
  116. package/src/facades/Validation.js +0 -69
@@ -3,878 +3,15 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta name="csrf-token" content="{{ csrfToken }}">
6
7
  <title>{% block title %}{{ pageTitle }}{% endblock %} — {{ adminTitle }}</title>
7
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
10
  <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
10
- <style>
11
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
12
-
13
- :root {
14
- /* ── Palette ── */
15
- --bg: #f4f5f7;
16
- --surface: #ffffff;
17
- --surface2: #f8f9fb;
18
- --surface3: #eef0f4;
19
- --border: #e3e6ec;
20
- --border-soft: #edf0f5;
21
-
22
- /* ── Brand ── */
23
- --primary: #2563eb;
24
- --primary-h: #1d4ed8;
25
- --primary-soft: #eff4ff;
26
- --primary-dim: #dbeafe;
27
-
28
- /* ── Text ── */
29
- --text: #111827;
30
- --text-soft: #374151;
31
- --text-muted: #6b7280;
32
- --text-xmuted: #9ca3af;
33
-
34
- /* ── Semantic ── */
35
- --success: #16a34a;
36
- --success-bg: #f0fdf4;
37
- --success-border:#bbf7d0;
38
- --danger: #dc2626;
39
- --danger-bg: #fef2f2;
40
- --danger-border:#fecaca;
41
- --warning: #d97706;
42
- --warning-bg: #fffbeb;
43
- --warning-border:#fed7aa;
44
- --info: #0284c7;
45
- --info-bg: #f0f9ff;
46
- --info-border: #bae6fd;
47
-
48
- /* ── Shape ── */
49
- --radius: 8px;
50
- --radius-sm: 6px;
51
- --radius-lg: 12px;
52
- --shadow-sm: 0 1px 3px rgba(0,0,0,.08), 0 1px 2px rgba(0,0,0,.04);
53
- --shadow: 0 4px 12px rgba(0,0,0,.08), 0 2px 4px rgba(0,0,0,.04);
54
- --shadow-lg: 0 12px 32px rgba(0,0,0,.12), 0 4px 8px rgba(0,0,0,.06);
55
-
56
- /* ── Sidebar ── */
57
- --sidebar-w: 232px;
58
- }
59
-
60
- body {
61
- font-family: 'DM Sans', system-ui, sans-serif;
62
- background: var(--bg);
63
- color: var(--text);
64
- display: flex;
65
- height: 100vh;
66
- overflow: hidden;
67
- font-size: 14px;
68
- line-height: 1.5;
69
- -webkit-font-smoothing: antialiased;
70
- }
71
-
72
- /* ════════════════════════════════════════
73
- ICONS (inline SVG sprite system)
74
- ════════════════════════════════════════ */
75
- .icon {
76
- display: inline-flex;
77
- align-items: center;
78
- justify-content: center;
79
- flex-shrink: 0;
80
- }
81
- .icon svg {
82
- width: 1em;
83
- height: 1em;
84
- fill: none;
85
- stroke: currentColor;
86
- stroke-width: 1.75;
87
- stroke-linecap: round;
88
- stroke-linejoin: round;
89
- }
90
- .icon-sm svg { stroke-width: 2; }
91
- .icon-14 { font-size: 14px; }
92
- .icon-15 { font-size: 15px; }
93
- .icon-16 { font-size: 16px; }
94
- .icon-18 { font-size: 18px; }
95
- .icon-20 { font-size: 20px; }
96
-
97
- /* ════════════════════════════════════════
98
- SIDEBAR
99
- ════════════════════════════════════════ */
100
- #sidebar {
101
- width: var(--sidebar-w);
102
- min-width: var(--sidebar-w);
103
- background: var(--surface);
104
- border-right: 1px solid var(--border);
105
- display: flex;
106
- flex-direction: column;
107
- overflow-y: auto;
108
- overflow-x: hidden;
109
- }
110
-
111
- .sidebar-brand {
112
- padding: 18px 16px 16px;
113
- border-bottom: 1px solid var(--border-soft);
114
- display: flex;
115
- align-items: center;
116
- gap: 10px;
117
- }
118
- .brand-logo {
119
- width: 34px; height: 34px;
120
- background: var(--primary);
121
- border-radius: 9px;
122
- display: flex; align-items: center; justify-content: center;
123
- color: #fff;
124
- font-size: 16px;
125
- flex-shrink: 0;
126
- box-shadow: 0 2px 8px rgba(37,99,235,.35);
127
- }
128
- .brand-text { line-height: 1.25; overflow: hidden; }
129
- .brand-name {
130
- font-size: 13.5px;
131
- font-weight: 700;
132
- color: var(--text);
133
- white-space: nowrap;
134
- overflow: hidden;
135
- text-overflow: ellipsis;
136
- }
137
- .brand-sub { font-size: 11px; color: var(--text-muted); }
138
-
139
- .nav-section { padding: 10px 10px 4px; }
140
- .nav-label {
141
- font-size: 10.5px;
142
- font-weight: 600;
143
- color: var(--text-xmuted);
144
- text-transform: uppercase;
145
- letter-spacing: 0.7px;
146
- padding: 0 8px 6px;
147
- }
148
- .nav-item {
149
- display: flex;
150
- align-items: center;
151
- gap: 9px;
152
- padding: 7px 10px;
153
- border-radius: var(--radius-sm);
154
- color: var(--text-muted);
155
- text-decoration: none;
156
- font-size: 13.5px;
157
- font-weight: 500;
158
- transition: background .12s, color .12s;
159
- cursor: pointer;
160
- border: none;
161
- background: none;
162
- width: 100%;
163
- text-align: left;
164
- }
165
- .nav-item:hover { background: var(--surface2); color: var(--text-soft); }
166
- .nav-item.active { background: var(--primary-soft); color: var(--primary); }
167
- .nav-item.active .nav-icon { color: var(--primary); }
168
-
169
- .nav-icon {
170
- color: var(--text-xmuted);
171
- transition: color .12s;
172
- width: 18px;
173
- display: flex;
174
- align-items: center;
175
- justify-content: center;
176
- flex-shrink: 0;
177
- }
178
- .nav-item:hover .nav-icon { color: var(--text-soft); }
179
- .nav-item.active .nav-icon { color: var(--primary); }
180
-
181
- .nav-count {
182
- margin-left: auto;
183
- font-size: 11px;
184
- font-weight: 500;
185
- background: var(--surface3);
186
- color: var(--text-muted);
187
- padding: 1px 6px;
188
- border-radius: 99px;
189
- min-width: 20px;
190
- text-align: center;
191
- }
192
-
193
- .sidebar-footer {
194
- margin-top: auto;
195
- padding: 12px 16px;
196
- border-top: 1px solid var(--border-soft);
197
- }
198
- .sidebar-version {
199
- font-size: 11px;
200
- color: var(--text-xmuted);
201
- }
202
-
203
- /* ── User row ── */
204
- .user-row {
205
- display: flex; align-items: center; gap: 9px;
206
- }
207
- .user-avatar {
208
- width: 30px; height: 30px; border-radius: 8px;
209
- background: var(--primary-dim); color: var(--primary);
210
- display: flex; align-items: center; justify-content: center;
211
- font-size: 13px; font-weight: 700; flex-shrink: 0;
212
- }
213
- .user-info { flex: 1; min-width: 0; }
214
- .user-name { font-size: 12.5px; font-weight: 600; color: var(--text-soft); }
215
- .user-email { font-size: 11px; color: var(--text-muted); }
216
- .logout-btn {
217
- flex-shrink: 0; padding: 5px;
218
- border-radius: 5px; color: var(--text-muted);
219
- display: flex; align-items: center;
220
- transition: background .1s, color .1s;
221
- }
222
- .logout-btn:hover { background: var(--surface3); color: var(--danger); }
223
-
224
- /* ════════════════════════════════════════
225
- MAIN AREA
226
- ════════════════════════════════════════ */
227
- #main {
228
- flex: 1;
229
- display: flex;
230
- flex-direction: column;
231
- overflow: hidden;
232
- min-width: 0;
233
- }
234
-
235
- /* ── Topbar ── */
236
- #topbar {
237
- background: var(--surface);
238
- border-bottom: 1px solid var(--border);
239
- padding: 0 24px;
240
- height: 54px;
241
- display: flex;
242
- align-items: center;
243
- gap: 12px;
244
- flex-shrink: 0;
245
- box-shadow: var(--shadow-sm);
246
- }
247
- .topbar-title {
248
- font-size: 15px;
249
- font-weight: 600;
250
- flex: 1;
251
- color: var(--text);
252
- display: flex;
253
- align-items: center;
254
- gap: 8px;
255
- }
256
- .topbar-actions { display: flex; gap: 8px; align-items: center; }
257
-
258
- /* ── Content ── */
259
- #content {
260
- flex: 1;
261
- overflow-y: auto;
262
- padding: 24px;
263
- }
264
-
265
- /* ════════════════════════════════════════
266
- BREADCRUMB
267
- ════════════════════════════════════════ */
268
- .breadcrumb {
269
- display: flex;
270
- align-items: center;
271
- gap: 5px;
272
- font-size: 12px;
273
- color: var(--text-muted);
274
- margin-bottom: 18px;
275
- }
276
- .breadcrumb a { color: var(--text-muted); text-decoration: none; }
277
- .breadcrumb a:hover { color: var(--primary); }
278
- .breadcrumb-sep { color: var(--border); }
279
- .breadcrumb-current { color: var(--text-soft); font-weight: 500; }
280
-
281
- /* ════════════════════════════════════════
282
- ALERTS
283
- ════════════════════════════════════════ */
284
- .alert {
285
- padding: 11px 16px;
286
- border-radius: var(--radius-sm);
287
- font-size: 13px;
288
- margin-bottom: 18px;
289
- display: flex;
290
- align-items: center;
291
- gap: 9px;
292
- border: 1px solid transparent;
293
- }
294
- .alert-success { background: var(--success-bg); color: var(--success); border-color: var(--success-border); }
295
- .alert-error { background: var(--danger-bg); color: var(--danger); border-color: var(--danger-border); }
296
- .alert-warning { background: var(--warning-bg); color: var(--warning); border-color: var(--warning-border); }
297
- .alert-info { background: var(--info-bg); color: var(--info); border-color: var(--info-border); }
298
- .alert-close {
299
- margin-left: auto;
300
- background: none;
301
- border: none;
302
- cursor: pointer;
303
- color: inherit;
304
- opacity: .6;
305
- padding: 0;
306
- line-height: 1;
307
- font-size: 16px;
308
- }
309
- .alert-close:hover { opacity: 1; }
310
-
311
- /* ════════════════════════════════════════
312
- CARDS
313
- ════════════════════════════════════════ */
314
- .card {
315
- background: var(--surface);
316
- border: 1px solid var(--border);
317
- border-radius: var(--radius-lg);
318
- overflow: hidden;
319
- box-shadow: var(--shadow-sm);
320
- }
321
- .card-header {
322
- padding: 14px 20px;
323
- border-bottom: 1px solid var(--border-soft);
324
- display: flex;
325
- align-items: center;
326
- justify-content: space-between;
327
- gap: 12px;
328
- flex-wrap: wrap;
329
- background: var(--surface);
330
- }
331
- .card-title {
332
- font-size: 13.5px;
333
- font-weight: 600;
334
- color: var(--text);
335
- display: flex;
336
- align-items: center;
337
- gap: 7px;
338
- }
339
- .card-body { padding: 20px; }
340
-
341
- /* ════════════════════════════════════════
342
- STAT CARDS
343
- ════════════════════════════════════════ */
344
- .stats-grid {
345
- display: grid;
346
- grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
347
- gap: 14px;
348
- margin-bottom: 22px;
349
- }
350
- .stat-card {
351
- background: var(--surface);
352
- border: 1px solid var(--border);
353
- border-radius: var(--radius-lg);
354
- padding: 18px 20px;
355
- display: flex;
356
- flex-direction: column;
357
- gap: 6px;
358
- transition: box-shadow .15s, border-color .15s;
359
- text-decoration: none;
360
- box-shadow: var(--shadow-sm);
361
- }
362
- .stat-card:hover {
363
- box-shadow: var(--shadow);
364
- border-color: var(--primary-dim);
365
- }
366
- .stat-icon-wrap {
367
- width: 36px; height: 36px;
368
- border-radius: 9px;
369
- background: var(--primary-soft);
370
- display: flex; align-items: center; justify-content: center;
371
- color: var(--primary);
372
- margin-bottom: 4px;
373
- }
374
- .stat-label {
375
- font-size: 11.5px;
376
- color: var(--text-muted);
377
- font-weight: 500;
378
- text-transform: uppercase;
379
- letter-spacing: 0.4px;
380
- }
381
- .stat-value {
382
- font-size: 26px;
383
- font-weight: 700;
384
- color: var(--text);
385
- line-height: 1;
386
- letter-spacing: -0.5px;
387
- }
388
- .stat-sub { font-size: 12px; color: var(--text-muted); }
389
-
390
- /* ════════════════════════════════════════
391
- TABLE
392
- ════════════════════════════════════════ */
393
- .table-wrap { overflow-x: auto; }
394
- table { width: 100%; border-collapse: collapse; }
395
- th {
396
- text-align: left;
397
- padding: 9px 16px;
398
- font-size: 11px;
399
- font-weight: 600;
400
- text-transform: uppercase;
401
- letter-spacing: 0.5px;
402
- color: var(--text-muted);
403
- background: var(--surface2);
404
- border-bottom: 1px solid var(--border);
405
- white-space: nowrap;
406
- }
407
- th.sortable { cursor: pointer; user-select: none; }
408
- th.sortable:hover { color: var(--text-soft); }
409
- th.sort-active { color: var(--primary); }
410
- .sort-indicator { display: inline-flex; flex-direction: column; gap: 1px; margin-left: 4px; vertical-align: middle; }
411
- .sort-indicator span { width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; opacity: .3; }
412
- .sort-indicator .up { border-bottom: 5px solid currentColor; }
413
- .sort-indicator .down { border-top: 5px solid currentColor; }
414
- th.sort-asc .sort-indicator .up { opacity: 1; }
415
- th.sort-desc .sort-indicator .down { opacity: 1; }
416
- td {
417
- padding: 11px 16px;
418
- font-size: 13px;
419
- border-bottom: 1px solid var(--border-soft);
420
- vertical-align: middle;
421
- color: var(--text-soft);
422
- }
423
- tr:last-child td { border-bottom: none; }
424
- tr:hover td { background: var(--surface2); }
425
- .col-check { width: 44px; }
426
- .col-actions { width: 100px; text-align: right; }
427
- .td-primary { color: var(--text); font-weight: 500; }
428
-
429
- /* ── Row checkbox ── */
430
- .row-check {
431
- width: 16px; height: 16px;
432
- cursor: pointer;
433
- accent-color: var(--primary);
434
- }
435
-
436
- /* ── Action menu ── */
437
- .action-menu { position: relative; display: inline-block; }
438
- .action-menu-btn {
439
- background: none;
440
- border: 1px solid var(--border);
441
- border-radius: var(--radius-sm);
442
- padding: 4px 8px;
443
- cursor: pointer;
444
- color: var(--text-muted);
445
- display: flex;
446
- align-items: center;
447
- gap: 3px;
448
- font-size: 12px;
449
- transition: all .12s;
450
- }
451
- .action-menu-btn:hover { background: var(--surface2); color: var(--text-soft); border-color: var(--border); }
452
- .action-dropdown {
453
- position: absolute;
454
- right: 0;
455
- top: calc(100% + 4px);
456
- background: var(--surface);
457
- border: 1px solid var(--border);
458
- border-radius: var(--radius);
459
- box-shadow: var(--shadow-lg);
460
- min-width: 140px;
461
- z-index: 50;
462
- overflow: hidden;
463
- display: none;
464
- }
465
- .action-dropdown.open { display: block; }
466
- .action-dropdown a,
467
- .action-dropdown button {
468
- display: flex;
469
- align-items: center;
470
- gap: 8px;
471
- padding: 8px 14px;
472
- font-size: 13px;
473
- color: var(--text-soft);
474
- text-decoration: none;
475
- background: none;
476
- border: none;
477
- width: 100%;
478
- text-align: left;
479
- cursor: pointer;
480
- font-family: inherit;
481
- transition: background .1s;
482
- }
483
- .action-dropdown a:hover,
484
- .action-dropdown button:hover { background: var(--surface2); }
485
- .action-dropdown .sep { height: 1px; background: var(--border-soft); margin: 3px 0; }
486
- .action-dropdown .danger { color: var(--danger); }
487
-
488
- /* ════════════════════════════════════════
489
- BADGES
490
- ════════════════════════════════════════ */
491
- .badge {
492
- display: inline-flex;
493
- align-items: center;
494
- gap: 4px;
495
- padding: 2px 8px;
496
- border-radius: 99px;
497
- font-size: 11.5px;
498
- font-weight: 600;
499
- white-space: nowrap;
500
- border: 1px solid transparent;
501
- }
502
- .badge-blue { background: #eff6ff; color: #1d4ed8; border-color: #bfdbfe; }
503
- .badge-red { background: #fef2f2; color: #b91c1c; border-color: #fecaca; }
504
- .badge-green { background: #f0fdf4; color: #15803d; border-color: #bbf7d0; }
505
- .badge-yellow { background: #fffbeb; color: #b45309; border-color: #fde68a; }
506
- .badge-purple { background: #faf5ff; color: #7e22ce; border-color: #e9d5ff; }
507
- .badge-gray { background: #f9fafb; color: #6b7280; border-color: #e5e7eb; }
508
- .badge-orange { background: #fff7ed; color: #c2410c; border-color: #fed7aa; }
509
-
510
- /* ════════════════════════════════════════
511
- BUTTONS
512
- ════════════════════════════════════════ */
513
- .btn {
514
- display: inline-flex;
515
- align-items: center;
516
- gap: 6px;
517
- padding: 7px 14px;
518
- border-radius: var(--radius-sm);
519
- font-size: 13px;
520
- font-weight: 500;
521
- cursor: pointer;
522
- border: 1px solid transparent;
523
- transition: all .12s;
524
- text-decoration: none;
525
- font-family: inherit;
526
- line-height: 1;
527
- white-space: nowrap;
528
- }
529
- .btn-primary {
530
- background: var(--primary);
531
- color: #fff;
532
- border-color: var(--primary);
533
- box-shadow: 0 1px 3px rgba(37,99,235,.3);
534
- }
535
- .btn-primary:hover { background: var(--primary-h); border-color: var(--primary-h); }
536
- .btn-ghost {
537
- background: transparent;
538
- color: var(--text-soft);
539
- border-color: var(--border);
540
- }
541
- .btn-ghost:hover { background: var(--surface2); color: var(--text); }
542
- .btn-danger {
543
- background: transparent;
544
- color: var(--danger);
545
- border-color: var(--border);
546
- }
547
- .btn-danger:hover { background: var(--danger-bg); border-color: var(--danger-border); }
548
- .btn-success {
549
- background: var(--success);
550
- color: #fff;
551
- border-color: var(--success);
552
- }
553
- .btn-success:hover { opacity: .9; }
554
- .btn-sm { padding: 5px 10px; font-size: 12px; }
555
- .btn-xs { padding: 3px 8px; font-size: 11px; }
556
- .btn-icon { padding: 6px; }
557
- .btn:disabled { opacity: .5; cursor: not-allowed; pointer-events: none; }
558
-
559
- /* ════════════════════════════════════════
560
- FORMS
561
- ════════════════════════════════════════ */
562
- .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
563
- .form-group { display: flex; flex-direction: column; gap: 5px; }
564
- .form-group.full { grid-column: 1 / -1; }
565
- .form-group.w-third { grid-column: span 1; }
566
- .form-label { font-size: 12.5px; font-weight: 500; color: var(--text-soft); }
567
- .form-label .required { color: var(--danger); margin-left: 2px; }
568
- .form-control {
569
- background: var(--surface);
570
- border: 1px solid var(--border);
571
- color: var(--text);
572
- border-radius: var(--radius-sm);
573
- padding: 8px 11px;
574
- font-size: 13.5px;
575
- width: 100%;
576
- outline: none;
577
- font-family: inherit;
578
- transition: border .12s, box-shadow .12s;
579
- }
580
- .form-control:hover { border-color: #c4c9d4; }
581
- .form-control:focus {
582
- border-color: var(--primary);
583
- box-shadow: 0 0 0 3px var(--primary-soft);
584
- }
585
- .form-control.error {
586
- border-color: var(--danger);
587
- box-shadow: 0 0 0 3px var(--danger-bg);
588
- }
589
- .form-control::placeholder { color: var(--text-xmuted); }
590
- select.form-control { cursor: pointer; }
591
- textarea.form-control { resize: vertical; min-height: 90px; }
592
- .form-help { font-size: 11.5px; color: var(--text-muted); }
593
- .form-error { font-size: 11.5px; color: var(--danger); display: flex; align-items: center; gap: 4px; }
594
-
595
- /* ── Checkbox / radio ── */
596
- .check-group { display: flex; align-items: center; gap: 8px; }
597
- .check-input {
598
- width: 16px; height: 16px;
599
- accent-color: var(--primary);
600
- cursor: pointer;
601
- }
602
- .check-label { font-size: 13.5px; color: var(--text-soft); cursor: pointer; }
603
-
604
- /* ── Search ── */
605
- .search-wrap { position: relative; }
606
- .search-icon-inner {
607
- position: absolute;
608
- left: 10px;
609
- top: 50%;
610
- transform: translateY(-50%);
611
- color: var(--text-muted);
612
- pointer-events: none;
613
- font-size: 14px;
614
- display: flex;
615
- }
616
- .search-input { padding-left: 34px !important; width: 220px; }
617
-
618
- /* ════════════════════════════════════════
619
- TOOLBAR (list page)
620
- ════════════════════════════════════════ */
621
- .toolbar {
622
- display: flex;
623
- align-items: center;
624
- gap: 8px;
625
- padding: 12px 18px;
626
- border-bottom: 1px solid var(--border-soft);
627
- flex-wrap: wrap;
628
- }
629
- .toolbar-left { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
630
- .toolbar-right { display: flex; align-items: center; gap: 8px; }
631
-
632
- /* ── Bulk action bar ── */
633
- .bulk-bar {
634
- display: none;
635
- align-items: center;
636
- gap: 10px;
637
- padding: 10px 18px;
638
- background: var(--primary-soft);
639
- border-bottom: 1px solid var(--primary-dim);
640
- font-size: 13px;
641
- }
642
- .bulk-bar.visible { display: flex; }
643
- .bulk-count { font-weight: 600; color: var(--primary); }
644
-
645
- /* ════════════════════════════════════════
646
- FILTER PANEL
647
- ════════════════════════════════════════ */
648
- .filter-row {
649
- display: flex;
650
- align-items: flex-end;
651
- gap: 10px;
652
- padding: 12px 18px;
653
- border-bottom: 1px solid var(--border-soft);
654
- flex-wrap: wrap;
655
- background: var(--surface2);
656
- }
657
- .filter-group { display: flex; flex-direction: column; gap: 4px; }
658
- .filter-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .4px; color: var(--text-muted); }
659
- .filter-control {
660
- background: var(--surface);
661
- border: 1px solid var(--border);
662
- color: var(--text);
663
- border-radius: var(--radius-sm);
664
- padding: 6px 10px;
665
- font-size: 12.5px;
666
- outline: none;
667
- font-family: inherit;
668
- transition: border .12s;
669
- }
670
- .filter-control:focus { border-color: var(--primary); }
671
-
672
- /* ════════════════════════════════════════
673
- PAGINATION
674
- ════════════════════════════════════════ */
675
- .pagination {
676
- display: flex;
677
- align-items: center;
678
- gap: 3px;
679
- padding: 12px 18px;
680
- border-top: 1px solid var(--border-soft);
681
- flex-wrap: wrap;
682
- }
683
- .page-info { font-size: 12px; color: var(--text-muted); margin-left: auto; }
684
- .page-btn {
685
- min-width: 30px;
686
- height: 30px;
687
- padding: 0 6px;
688
- display: inline-flex;
689
- align-items: center;
690
- justify-content: center;
691
- border-radius: var(--radius-sm);
692
- border: 1px solid var(--border);
693
- background: transparent;
694
- color: var(--text-soft);
695
- cursor: pointer;
696
- font-size: 12px;
697
- font-family: inherit;
698
- transition: all .12s;
699
- text-decoration: none;
700
- }
701
- .page-btn:hover:not(:disabled) { background: var(--surface2); color: var(--text); border-color: #c4c9d4; }
702
- .page-btn.active { background: var(--primary); border-color: var(--primary); color: #fff; }
703
- .page-btn:disabled { opacity: .35; cursor: not-allowed; }
704
- .page-ellipsis { color: var(--text-muted); font-size: 12px; padding: 0 3px; }
705
-
706
- /* ════════════════════════════════════════
707
- MODAL
708
- ════════════════════════════════════════ */
709
- .modal-overlay {
710
- position: fixed;
711
- inset: 0;
712
- background: rgba(17,24,39,.5);
713
- display: flex;
714
- align-items: center;
715
- justify-content: center;
716
- z-index: 200;
717
- padding: 24px;
718
- opacity: 0;
719
- pointer-events: none;
720
- transition: opacity .18s;
721
- backdrop-filter: blur(2px);
722
- }
723
- .modal-overlay.open { opacity: 1; pointer-events: all; }
724
- .modal {
725
- background: var(--surface);
726
- border: 1px solid var(--border);
727
- border-radius: var(--radius-lg);
728
- width: 100%;
729
- max-width: 520px;
730
- max-height: 90vh;
731
- overflow-y: auto;
732
- transform: translateY(10px) scale(.99);
733
- transition: transform .2s;
734
- box-shadow: var(--shadow-lg);
735
- }
736
- .modal-overlay.open .modal { transform: translateY(0) scale(1); }
737
- .modal-sm { max-width: 400px; }
738
- .modal-lg { max-width: 700px; }
739
- .modal-header {
740
- padding: 18px 22px;
741
- border-bottom: 1px solid var(--border-soft);
742
- display: flex;
743
- justify-content: space-between;
744
- align-items: center;
745
- position: sticky;
746
- top: 0;
747
- background: var(--surface);
748
- z-index: 1;
749
- }
750
- .modal-title { font-size: 15px; font-weight: 600; color: var(--text); }
751
- .modal-body { padding: 20px 22px; }
752
- .modal-footer {
753
- padding: 14px 22px;
754
- border-top: 1px solid var(--border-soft);
755
- display: flex;
756
- justify-content: flex-end;
757
- gap: 8px;
758
- position: sticky;
759
- bottom: 0;
760
- background: var(--surface);
761
- }
762
- .close-btn {
763
- background: none;
764
- border: none;
765
- color: var(--text-muted);
766
- cursor: pointer;
767
- padding: 4px;
768
- line-height: 1;
769
- border-radius: 4px;
770
- display: flex;
771
- align-items: center;
772
- transition: background .1s, color .1s;
773
- }
774
- .close-btn:hover { background: var(--surface3); color: var(--text); }
775
-
776
- /* ════════════════════════════════════════
777
- EMPTY STATE
778
- ════════════════════════════════════════ */
779
- .empty-state {
780
- text-align: center;
781
- padding: 56px 24px;
782
- color: var(--text-muted);
783
- }
784
- .empty-icon {
785
- width: 52px;
786
- height: 52px;
787
- background: var(--surface3);
788
- border-radius: 14px;
789
- display: flex;
790
- align-items: center;
791
- justify-content: center;
792
- margin: 0 auto 16px;
793
- color: var(--text-xmuted);
794
- }
795
- .empty-title { font-size: 15px; font-weight: 600; color: var(--text-soft); margin-bottom: 7px; }
796
- .empty-desc { font-size: 13px; max-width: 320px; margin: 0 auto 20px; line-height: 1.6; }
797
-
798
- /* ════════════════════════════════════════
799
- CELL TYPES
800
- ════════════════════════════════════════ */
801
- .bool-yes {
802
- display: inline-flex; align-items: center; justify-content: center;
803
- width: 20px; height: 20px; border-radius: 99px;
804
- background: var(--success-bg); color: var(--success);
805
- }
806
- .bool-no {
807
- display: inline-flex; align-items: center; justify-content: center;
808
- width: 20px; height: 20px; border-radius: 99px;
809
- background: var(--danger-bg); color: var(--danger);
810
- }
811
- .cell-muted { color: var(--text-xmuted); font-style: italic; }
812
- .cell-mono { font-family: 'DM Mono', monospace; font-size: 12px; }
813
- .cell-image { width: 34px; height: 34px; border-radius: 7px; object-fit: cover; border: 1px solid var(--border); }
814
-
815
- /* ════════════════════════════════════════
816
- TOAST
817
- ════════════════════════════════════════ */
818
- #toast-root {
819
- position: fixed;
820
- bottom: 22px;
821
- right: 22px;
822
- display: flex;
823
- flex-direction: column;
824
- gap: 8px;
825
- z-index: 500;
826
- pointer-events: none;
827
- }
828
- .toast {
829
- background: var(--text);
830
- color: #fff;
831
- border-radius: var(--radius);
832
- padding: 11px 16px;
833
- font-size: 13px;
834
- max-width: 320px;
835
- box-shadow: var(--shadow-lg);
836
- pointer-events: all;
837
- display: flex;
838
- align-items: center;
839
- gap: 9px;
840
- animation: toastIn .2s ease;
841
- }
842
- .toast-success .toast-dot { color: #4ade80; }
843
- .toast-error .toast-dot { color: #f87171; }
844
- @keyframes toastIn { from { transform: translateX(16px); opacity: 0; } }
845
-
846
- /* ════════════════════════════════════════
847
- UTILITIES
848
- ════════════════════════════════════════ */
849
- .flex { display: flex; }
850
- .flex-col { flex-direction: column; }
851
- .items-center { align-items: center; }
852
- .justify-between { justify-content: space-between; }
853
- .gap-1 { gap: 4px; }
854
- .gap-2 { gap: 8px; }
855
- .gap-3 { gap: 12px; }
856
- .gap-4 { gap: 16px; }
857
- .ml-auto { margin-left: auto; }
858
- .text-muted { color: var(--text-muted); }
859
- .text-sm { font-size: 12px; }
860
- .text-xs { font-size: 11px; }
861
- .fw-500 { font-weight: 500; }
862
- .fw-600 { font-weight: 600; }
863
- .mb-3 { margin-bottom: 12px; }
864
- .mb-4 { margin-bottom: 16px; }
865
- .mb-5 { margin-bottom: 20px; }
866
- .mb-6 { margin-bottom: 24px; }
867
- .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
868
-
869
- /* ════════════════════════════════════════
870
- RESPONSIVE
871
- ════════════════════════════════════════ */
872
- @media (max-width: 768px) {
873
- #sidebar { display: none; }
874
- .form-grid { grid-template-columns: 1fr; }
875
- .search-input { width: 160px; }
876
- }
877
- </style>
11
+ <link rel="stylesheet" href="{{ adminPrefix }}/static/admin.css?v=15">
12
+ <link rel="stylesheet" href="{{ adminPrefix }}/static/json-editor.css?v=4">
13
+ <link rel="stylesheet" href="{{ adminPrefix }}/static/date-picker.css?v=3">
14
+ <script src="https://code.jquery.com/jquery-3.7.1.min.js" crossorigin="anonymous"></script>
878
15
  {% block head %}{% endblock %}
879
16
  </head>
880
17
  <body>
@@ -1035,186 +172,101 @@
1035
172
 
1036
173
  <div id="toast-root"></div>
1037
174
 
175
+ <script src="{{ adminPrefix }}/static/ui.js?v=13"></script>
1038
176
  <script>
1039
- // ── Toast ────────────────────────────────────────────────────
1040
- function toast(msg, type = 'success') {
1041
- const icons = {
1042
- success: '<polyline points="20 6 9 17 4 12"/>',
1043
- error: '<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>',
1044
- };
1045
- const el = document.createElement('div');
1046
- el.className = `toast toast-${type}`;
1047
- el.innerHTML = `
1048
- <span class="toast-dot icon icon-15" style="flex-shrink:0">
1049
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1050
- ${icons[type] || icons.success}
1051
- </svg>
1052
- </span>
1053
- <span>${msg}</span>`;
1054
- document.getElementById('toast-root').appendChild(el);
1055
- setTimeout(() => el.style.opacity = '0', 3000);
1056
- setTimeout(() => el.remove(), 3300);
177
+ // ── Global helpers ─────────────────────────────────────────────────────────
178
+ function toast(msg, type) { UI.Toast.show(msg, type || 'success'); }
179
+ function openModal(id) { $('#' + id).addClass('open'); }
180
+ function closeModal(id) { id ? $('#' + id).removeClass('open') : $('.modal-overlay').removeClass('open'); }
181
+
182
+ // ── Delete confirmation ─────────────────────────────────────────────────────
183
+ function confirmDelete(url, label) {
184
+ UI.Confirm.show({
185
+ title: 'Delete ' + label,
186
+ message: 'Are you sure you want to delete <strong>' + label + '</strong>? This cannot be undone.',
187
+ confirm: 'Delete',
188
+ danger: true,
189
+ }).then(function(ok) { if (ok) submitDelete(url); });
190
+ }
191
+ function submitDelete(url) {
192
+ var csrf = $('meta[name="csrf-token"]').attr('content') || '';
193
+ $('<form method="POST">')
194
+ .attr('action', url)
195
+ .append('<input name="_method" value="DELETE">')
196
+ .append('<input name="_csrf" value="' + csrf + '">')
197
+ .appendTo('body')
198
+ .submit();
1057
199
  }
1058
200
 
1059
- // ── Modal ────────────────────────────────────────────────────
1060
- function openModal(id) { document.getElementById(id)?.classList.add('open'); }
1061
- function closeModal(id) {
1062
- if (id) document.getElementById(id)?.classList.remove('open');
1063
- else document.querySelectorAll('.modal-overlay').forEach(m => m.classList.remove('open'));
201
+ // ── M2M dual-list helpers ───────────────────────────────────────────────────
202
+ function m2mMove(fieldName, fromId, toId) {
203
+ $('#m2m-' + fromId + '-' + fieldName + ' option:selected')
204
+ .appendTo('#m2m-' + toId + '-' + fieldName);
1064
205
  }
1065
- document.addEventListener('click', e => {
1066
- if (e.target.classList.contains('modal-overlay')) closeModal();
1067
- });
1068
- document.addEventListener('keydown', e => {
1069
- if (e.key === 'Escape') closeModal();
1070
- });
1071
206
 
1072
- // ── Action dropdowns ────────────────────────────────────────
1073
- document.addEventListener('click', e => {
1074
- const btn = e.target.closest('.action-menu-btn');
1075
- if (btn) {
1076
- e.stopPropagation();
1077
- const menu = btn.nextElementSibling;
1078
- const isOpen = menu.classList.contains('open');
1079
- document.querySelectorAll('.action-dropdown.open').forEach(d => d.classList.remove('open'));
1080
- if (!isOpen) menu.classList.add('open');
1081
- return;
1082
- }
1083
- if (!e.target.closest('.action-dropdown')) {
1084
- document.querySelectorAll('.action-dropdown.open').forEach(d => d.classList.remove('open'));
207
+ $(function() {
208
+ // ── M2M: select all chosen before submit ──────────────────────────────────
209
+ $('form').on('submit', function() {
210
+ $('[id^="m2m-chosen-"] option').prop('selected', true);
211
+ });
212
+
213
+ // ── Flash auto-dismiss ────────────────────────────────────────────────────
214
+ var $flash = $('#flash-alert');
215
+ if ($flash.length) {
216
+ setTimeout(function() {
217
+ $flash.fadeTo(400, 0, function() { $flash.remove(); });
218
+ }, 5000);
1085
219
  }
1086
- });
1087
220
 
1088
- // ── Delete confirmation ──────────────────────────────────────
1089
- function confirmDelete(url, label) {
1090
- const overlay = document.createElement('div');
1091
- overlay.className = 'modal-overlay open';
1092
- overlay.innerHTML = `
1093
- <div class="modal modal-sm">
1094
- <div class="modal-header">
1095
- <span class="modal-title flex items-center gap-2">
1096
- <span class="icon icon-16" style="color:var(--danger)">
1097
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1098
- <polyline points="3 6 5 6 21 6"/>
1099
- <path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6M10 11v6M14 11v6"/>
1100
- <path d="M9 6V4a1 1 0 011-1h4a1 1 0 011 1v2"/>
1101
- </svg>
1102
- </span>
1103
- Delete ${label}
1104
- </span>
1105
- <button class="close-btn" onclick="this.closest('.modal-overlay').remove()">
1106
- <span class="icon icon-16"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></span>
1107
- </button>
1108
- </div>
1109
- <div class="modal-body">
1110
- <p style="color:var(--text-soft);line-height:1.6">
1111
- Are you sure you want to delete <strong style="color:var(--text)">${label}</strong>?
1112
- This action cannot be undone.
1113
- </p>
1114
- </div>
1115
- <div class="modal-footer">
1116
- <button class="btn btn-ghost" onclick="this.closest('.modal-overlay').remove()">Cancel</button>
1117
- <button class="btn btn-danger" onclick="submitDelete('${url}')">
1118
- <span class="icon icon-14"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/></svg></span>
1119
- Delete
1120
- </button>
1121
- </div>
1122
- </div>`;
1123
- overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
1124
- document.body.appendChild(overlay);
1125
- }
1126
- function submitDelete(url) {
1127
- const form = document.createElement('form');
1128
- form.method = 'POST';
1129
- form.action = url;
1130
- form.innerHTML = '<input name="_method" value="DELETE">';
1131
- document.body.appendChild(form);
1132
- form.submit();
1133
- }
221
+ // ── Search placeholder hint ───────────────────────────────────────────────
222
+ $('#global-search-input')
223
+ .on('focus', function() { $(this).attr('placeholder', 'Search everything…'); })
224
+ .on('blur', function() { $(this).attr('placeholder', 'Search everything… (/)'); });
1134
225
 
1135
- // ── Keyboard shortcuts ───────────────────────────────────────
1136
- (function() {
1137
- const prefix = '{{ adminPrefix }}';
1138
- const resources = [
226
+ // ── Keyboard shortcuts ────────────────────────────────────────────────────
227
+ var prefix = '{{ adminPrefix }}';
228
+ var resources = [
1139
229
  {% for r in resources %}
1140
- { slug: '{{ r.slug }}', index: {{ r.index | default('null')}} }{% if not loop.last %},{% endif %}
230
+ { slug: '{{ r.slug }}', index: {{ r.index | default('null') }} }{% if not loop.last %},{% endif %}
1141
231
  {% endfor %}
1142
232
  ];
233
+ var gPressed = false, gTimer;
1143
234
 
1144
- let gPressed = false;
1145
- let gTimer = null;
1146
-
1147
- document.addEventListener('keydown', function(e) {
1148
- // Don't fire when typing in inputs
1149
- const tag = document.activeElement?.tagName;
235
+ $(document).on('keydown', function(e) {
236
+ var tag = document.activeElement && document.activeElement.tagName;
1150
237
  if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {
1151
- // Allow Escape to blur
1152
- if (e.key === 'Escape') document.activeElement.blur();
238
+ if (e.key === 'Escape') $(document.activeElement).blur();
1153
239
  return;
1154
240
  }
1155
-
1156
- // / → focus global search
1157
241
  if (e.key === '/') {
1158
242
  e.preventDefault();
1159
- document.getElementById('global-search-input')?.focus();
243
+ $('#global-search-input').focus();
1160
244
  return;
1161
245
  }
1162
-
1163
- // N → new record (only on list pages)
1164
246
  if (e.key === 'n' || e.key === 'N') {
1165
- const newBtn = document.querySelector('a[href*="/create"].btn-primary');
1166
- if (newBtn) { e.preventDefault(); window.location.href = newBtn.href; }
247
+ var $btn = $('a[href*="/create"].btn-primary').first();
248
+ if ($btn.length) { e.preventDefault(); location.href = $btn.attr('href'); }
1167
249
  return;
1168
250
  }
1169
-
1170
- // Escape → close modals (already handled), clear selection
1171
- if (e.key === 'Escape') {
1172
- document.querySelectorAll('.modal-overlay').forEach(m => m.classList.remove('open'));
1173
- return;
1174
- }
1175
-
1176
- // G → start chord
251
+ if (e.key === 'Escape') { $('.modal-overlay').removeClass('open'); return; }
1177
252
  if (e.key === 'g' || e.key === 'G') {
1178
253
  if (gPressed) return;
1179
254
  gPressed = true;
1180
255
  clearTimeout(gTimer);
1181
- gTimer = setTimeout(() => { gPressed = false; }, 800);
256
+ gTimer = setTimeout(function() { gPressed = false; }, 800);
1182
257
  return;
1183
258
  }
1184
-
1185
- // G+D → dashboard
1186
259
  if (gPressed && (e.key === 'd' || e.key === 'D')) {
1187
- gPressed = false;
1188
- window.location.href = prefix + '/';
1189
- return;
260
+ gPressed = false; location.href = prefix + '/'; return;
1190
261
  }
1191
-
1192
- // G+1…9 → jump to resource
1193
262
  if (gPressed && e.key >= '1' && e.key <= '9') {
1194
263
  gPressed = false;
1195
- const idx = parseInt(e.key);
1196
- const r = resources.find(x => x.index === idx);
1197
- if (r) window.location.href = `${prefix}/${r.slug}`;
1198
- return;
264
+ var idx = parseInt(e.key);
265
+ var r = $.grep(resources, function(x) { return x.index === idx; })[0];
266
+ if (r) location.href = prefix + '/' + r.slug;
1199
267
  }
1200
268
  });
1201
- })();
1202
-
1203
- // ── Shortcut hint tooltip ────────────────────────────────────
1204
- document.getElementById('global-search-input')?.addEventListener('focus', function() {
1205
- this.placeholder = 'Search everything…';
1206
269
  });
1207
- document.getElementById('global-search-input')?.addEventListener('blur', function() {
1208
- this.placeholder = 'Search everything… (/)';
1209
- });
1210
- const flashAlert = document.getElementById('flash-alert');
1211
- if (flashAlert) {
1212
- setTimeout(() => {
1213
- flashAlert.style.transition = 'opacity .4s';
1214
- flashAlert.style.opacity = '0';
1215
- setTimeout(() => flashAlert.remove(), 400);
1216
- }, 5000);
1217
- }
1218
270
  </script>
1219
271
  {% block scripts %}{% endblock %}
1220
272
  </body>