millas 0.1.9 → 0.2.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.
@@ -4,30 +4,61 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>{% block title %}{{ pageTitle }}{% endblock %} — {{ adminTitle }}</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <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">
7
10
  <style>
8
11
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
12
+
9
13
  :root {
10
- --bg: #0c0e14;
11
- --surface: #13151f;
12
- --surface2: #1c1f2e;
13
- --surface3: #252840;
14
- --border: #2a2d3e;
15
- --primary: #6366f1;
16
- --primary-h: #818cf8;
17
- --primary-dim: #1e1f3a;
18
- --text: #e2e8f0;
19
- --text-muted: #64748b;
20
- --text-soft: #94a3b8;
21
- --success: #22c55e;
22
- --danger: #ef4444;
23
- --warning: #f59e0b;
24
- --info: #38bdf8;
25
- --radius: 10px;
26
- --radius-sm: 6px;
27
- --shadow: 0 4px 24px rgba(0,0,0,.4);
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;
28
58
  }
59
+
29
60
  body {
30
- font-family: 'Inter', system-ui, -apple-system, sans-serif;
61
+ font-family: 'DM Sans', system-ui, sans-serif;
31
62
  background: var(--bg);
32
63
  color: var(--text);
33
64
  display: flex;
@@ -35,324 +66,842 @@
35
66
  overflow: hidden;
36
67
  font-size: 14px;
37
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;
38
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; }
39
96
 
40
- /* ── Sidebar ─────────────────────────────────────────────────── */
97
+ /* ════════════════════════════════════════
98
+ SIDEBAR
99
+ ════════════════════════════════════════ */
41
100
  #sidebar {
42
- width: 256px; min-width: 256px;
101
+ width: var(--sidebar-w);
102
+ min-width: var(--sidebar-w);
43
103
  background: var(--surface);
44
104
  border-right: 1px solid var(--border);
45
- display: flex; flex-direction: column;
46
- overflow-y: auto; overflow-x: hidden;
105
+ display: flex;
106
+ flex-direction: column;
107
+ overflow-y: auto;
108
+ overflow-x: hidden;
47
109
  }
110
+
48
111
  .sidebar-brand {
49
- padding: 20px 20px 16px;
50
- border-bottom: 1px solid var(--border);
51
- display: flex; align-items: center; gap: 10px;
112
+ padding: 18px 16px 16px;
113
+ border-bottom: 1px solid var(--border-soft);
114
+ display: flex;
115
+ align-items: center;
116
+ gap: 10px;
52
117
  }
53
- .brand-icon {
54
- width: 32px; height: 32px;
55
- background: linear-gradient(135deg, var(--primary), #a855f7);
56
- border-radius: 8px;
118
+ .brand-logo {
119
+ width: 34px; height: 34px;
120
+ background: var(--primary);
121
+ border-radius: 9px;
57
122
  display: flex; align-items: center; justify-content: center;
58
- font-size: 16px; flex-shrink: 0;
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;
59
136
  }
60
- .brand-text { line-height: 1.2; }
61
- .brand-name { font-size: 14px; font-weight: 700; color: var(--text); }
62
- .brand-sub { font-size: 11px; color: var(--text-muted); }
137
+ .brand-sub { font-size: 11px; color: var(--text-muted); }
63
138
 
64
- .nav-section { padding: 12px 12px 4px; }
139
+ .nav-section { padding: 10px 10px 4px; }
65
140
  .nav-label {
66
- font-size: 10px; font-weight: 600;
67
- color: var(--text-muted); text-transform: uppercase;
68
- letter-spacing: 0.8px; padding: 0 8px 8px;
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;
69
147
  }
70
148
  .nav-item {
71
- display: flex; align-items: center; gap: 10px;
72
- padding: 8px 12px; border-radius: var(--radius-sm);
73
- color: var(--text-soft); text-decoration: none;
74
- font-size: 13px; font-weight: 500;
75
- transition: all .15s; cursor: pointer;
76
- border: none; background: none; width: 100%; text-align: left;
77
- }
78
- .nav-item:hover { background: var(--surface2); color: var(--text); }
79
- .nav-item.active { background: var(--primary-dim); color: var(--primary-h); }
80
- .nav-icon { font-size: 15px; width: 20px; text-align: center; flex-shrink: 0; }
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
+
81
181
  .nav-count {
82
- margin-left: auto; font-size: 11px;
83
- background: var(--surface3); color: var(--text-muted);
84
- padding: 1px 6px; border-radius: 99px;
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;
85
191
  }
86
192
 
87
193
  .sidebar-footer {
88
194
  margin-top: auto;
89
- padding: 16px;
90
- border-top: 1px solid var(--border);
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);
91
201
  }
92
- .sidebar-version { font-size: 11px; color: var(--text-muted); text-align: center; }
93
202
 
94
- /* ── Main ────────────────────────────────────────────────────── */
95
- #main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
203
+ /* ════════════════════════════════════════
204
+ MAIN AREA
205
+ ════════════════════════════════════════ */
206
+ #main {
207
+ flex: 1;
208
+ display: flex;
209
+ flex-direction: column;
210
+ overflow: hidden;
211
+ min-width: 0;
212
+ }
96
213
 
214
+ /* ── Topbar ── */
97
215
  #topbar {
98
216
  background: var(--surface);
99
217
  border-bottom: 1px solid var(--border);
100
- padding: 0 24px; height: 56px;
101
- display: flex; align-items: center;
102
- gap: 12px; flex-shrink: 0;
218
+ padding: 0 24px;
219
+ height: 54px;
220
+ display: flex;
221
+ align-items: center;
222
+ gap: 12px;
223
+ flex-shrink: 0;
224
+ box-shadow: var(--shadow-sm);
225
+ }
226
+ .topbar-title {
227
+ font-size: 15px;
228
+ font-weight: 600;
229
+ flex: 1;
230
+ color: var(--text);
231
+ display: flex;
232
+ align-items: center;
233
+ gap: 8px;
103
234
  }
104
- .topbar-title { font-size: 15px; font-weight: 600; flex: 1; }
105
235
  .topbar-actions { display: flex; gap: 8px; align-items: center; }
106
236
 
107
- #content { flex: 1; overflow-y: auto; padding: 24px; }
237
+ /* ── Content ── */
238
+ #content {
239
+ flex: 1;
240
+ overflow-y: auto;
241
+ padding: 24px;
242
+ }
108
243
 
109
- /* ── Breadcrumb ──────────────────────────────────────────────── */
244
+ /* ════════════════════════════════════════
245
+ BREADCRUMB
246
+ ════════════════════════════════════════ */
110
247
  .breadcrumb {
111
- display: flex; align-items: center; gap: 6px;
112
- font-size: 12px; color: var(--text-muted);
113
- margin-bottom: 20px;
248
+ display: flex;
249
+ align-items: center;
250
+ gap: 5px;
251
+ font-size: 12px;
252
+ color: var(--text-muted);
253
+ margin-bottom: 18px;
114
254
  }
115
255
  .breadcrumb a { color: var(--text-muted); text-decoration: none; }
116
- .breadcrumb a:hover { color: var(--text); }
256
+ .breadcrumb a:hover { color: var(--primary); }
117
257
  .breadcrumb-sep { color: var(--border); }
258
+ .breadcrumb-current { color: var(--text-soft); font-weight: 500; }
118
259
 
119
- /* ── Alert ───────────────────────────────────────────────────── */
260
+ /* ════════════════════════════════════════
261
+ ALERTS
262
+ ════════════════════════════════════════ */
120
263
  .alert {
121
- padding: 12px 16px; border-radius: var(--radius-sm);
122
- font-size: 13px; margin-bottom: 20px;
123
- display: flex; align-items: center; gap: 8px;
264
+ padding: 11px 16px;
265
+ border-radius: var(--radius-sm);
266
+ font-size: 13px;
267
+ margin-bottom: 18px;
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 9px;
271
+ border: 1px solid transparent;
124
272
  }
125
- .alert-success { background: #0d2818; color: #4ade80; border: 1px solid #166534; }
126
- .alert-error { background: #2d0e0e; color: #f87171; border: 1px solid #7f1d1d; }
127
- .alert-warning { background: #2d1f00; color: #fbbf24; border: 1px solid #92400e; }
128
- .alert-info { background: #0d2233; color: #7dd3fc; border: 1px solid #075985; }
273
+ .alert-success { background: var(--success-bg); color: var(--success); border-color: var(--success-border); }
274
+ .alert-error { background: var(--danger-bg); color: var(--danger); border-color: var(--danger-border); }
275
+ .alert-warning { background: var(--warning-bg); color: var(--warning); border-color: var(--warning-border); }
276
+ .alert-info { background: var(--info-bg); color: var(--info); border-color: var(--info-border); }
277
+ .alert-close {
278
+ margin-left: auto;
279
+ background: none;
280
+ border: none;
281
+ cursor: pointer;
282
+ color: inherit;
283
+ opacity: .6;
284
+ padding: 0;
285
+ line-height: 1;
286
+ font-size: 16px;
287
+ }
288
+ .alert-close:hover { opacity: 1; }
129
289
 
130
- /* ── Cards ───────────────────────────────────────────────────── */
290
+ /* ════════════════════════════════════════
291
+ CARDS
292
+ ════════════════════════════════════════ */
131
293
  .card {
132
294
  background: var(--surface);
133
295
  border: 1px solid var(--border);
134
- border-radius: var(--radius);
296
+ border-radius: var(--radius-lg);
135
297
  overflow: hidden;
298
+ box-shadow: var(--shadow-sm);
136
299
  }
137
300
  .card-header {
138
- padding: 16px 20px;
139
- border-bottom: 1px solid var(--border);
140
- display: flex; align-items: center;
141
- justify-content: space-between; gap: 12px; flex-wrap: wrap;
301
+ padding: 14px 20px;
302
+ border-bottom: 1px solid var(--border-soft);
303
+ display: flex;
304
+ align-items: center;
305
+ justify-content: space-between;
306
+ gap: 12px;
307
+ flex-wrap: wrap;
308
+ background: var(--surface);
309
+ }
310
+ .card-title {
311
+ font-size: 13.5px;
312
+ font-weight: 600;
313
+ color: var(--text);
314
+ display: flex;
315
+ align-items: center;
316
+ gap: 7px;
142
317
  }
143
- .card-title { font-size: 14px; font-weight: 600; }
144
- .card-body { padding: 20px; }
318
+ .card-body { padding: 20px; }
145
319
 
146
- /* ── Stat Cards ──────────────────────────────────────────────── */
320
+ /* ════════════════════════════════════════
321
+ STAT CARDS
322
+ ════════════════════════════════════════ */
147
323
  .stats-grid {
148
324
  display: grid;
149
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
150
- gap: 16px; margin-bottom: 24px;
325
+ grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
326
+ gap: 14px;
327
+ margin-bottom: 22px;
151
328
  }
152
329
  .stat-card {
153
330
  background: var(--surface);
154
331
  border: 1px solid var(--border);
155
- border-radius: var(--radius);
156
- padding: 20px;
157
- display: flex; flex-direction: column; gap: 8px;
332
+ border-radius: var(--radius-lg);
333
+ padding: 18px 20px;
334
+ display: flex;
335
+ flex-direction: column;
336
+ gap: 6px;
337
+ transition: box-shadow .15s, border-color .15s;
338
+ text-decoration: none;
339
+ box-shadow: var(--shadow-sm);
158
340
  }
159
- .stat-label { font-size: 12px; color: var(--text-muted); font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px; }
160
- .stat-value { font-size: 28px; font-weight: 700; color: var(--text); line-height: 1; }
161
- .stat-icon { font-size: 24px; margin-bottom: 4px; }
162
- .stat-sub { font-size: 12px; color: var(--text-muted); }
341
+ .stat-card:hover {
342
+ box-shadow: var(--shadow);
343
+ border-color: var(--primary-dim);
344
+ }
345
+ .stat-icon-wrap {
346
+ width: 36px; height: 36px;
347
+ border-radius: 9px;
348
+ background: var(--primary-soft);
349
+ display: flex; align-items: center; justify-content: center;
350
+ color: var(--primary);
351
+ margin-bottom: 4px;
352
+ }
353
+ .stat-label {
354
+ font-size: 11.5px;
355
+ color: var(--text-muted);
356
+ font-weight: 500;
357
+ text-transform: uppercase;
358
+ letter-spacing: 0.4px;
359
+ }
360
+ .stat-value {
361
+ font-size: 26px;
362
+ font-weight: 700;
363
+ color: var(--text);
364
+ line-height: 1;
365
+ letter-spacing: -0.5px;
366
+ }
367
+ .stat-sub { font-size: 12px; color: var(--text-muted); }
163
368
 
164
- /* ── Table ───────────────────────────────────────────────────── */
369
+ /* ════════════════════════════════════════
370
+ TABLE
371
+ ════════════════════════════════════════ */
165
372
  .table-wrap { overflow-x: auto; }
166
373
  table { width: 100%; border-collapse: collapse; }
167
374
  th {
168
- text-align: left; padding: 10px 16px;
169
- font-size: 11px; font-weight: 600;
170
- text-transform: uppercase; letter-spacing: 0.5px;
171
- color: var(--text-muted); background: var(--surface2);
375
+ text-align: left;
376
+ padding: 9px 16px;
377
+ font-size: 11px;
378
+ font-weight: 600;
379
+ text-transform: uppercase;
380
+ letter-spacing: 0.5px;
381
+ color: var(--text-muted);
382
+ background: var(--surface2);
172
383
  border-bottom: 1px solid var(--border);
173
384
  white-space: nowrap;
174
385
  }
175
386
  th.sortable { cursor: pointer; user-select: none; }
176
- th.sortable:hover { color: var(--text); }
177
- th.sort-asc::after { content: ' ↑'; color: var(--primary-h); }
178
- th.sort-desc::after { content: ' ↓'; color: var(--primary-h); }
387
+ th.sortable:hover { color: var(--text-soft); }
388
+ th.sort-active { color: var(--primary); }
389
+ .sort-indicator { display: inline-flex; flex-direction: column; gap: 1px; margin-left: 4px; vertical-align: middle; }
390
+ .sort-indicator span { width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; opacity: .3; }
391
+ .sort-indicator .up { border-bottom: 5px solid currentColor; }
392
+ .sort-indicator .down { border-top: 5px solid currentColor; }
393
+ th.sort-asc .sort-indicator .up { opacity: 1; }
394
+ th.sort-desc .sort-indicator .down { opacity: 1; }
179
395
  td {
180
- padding: 12px 16px; font-size: 13px;
181
- border-bottom: 1px solid var(--border);
396
+ padding: 11px 16px;
397
+ font-size: 13px;
398
+ border-bottom: 1px solid var(--border-soft);
182
399
  vertical-align: middle;
400
+ color: var(--text-soft);
183
401
  }
184
402
  tr:last-child td { border-bottom: none; }
185
403
  tr:hover td { background: var(--surface2); }
186
- .col-actions { width: 120px; text-align: right; }
404
+ .col-check { width: 44px; }
405
+ .col-actions { width: 100px; text-align: right; }
406
+ .td-primary { color: var(--text); font-weight: 500; }
407
+
408
+ /* ── Row checkbox ── */
409
+ .row-check {
410
+ width: 16px; height: 16px;
411
+ cursor: pointer;
412
+ accent-color: var(--primary);
413
+ }
414
+
415
+ /* ── Action menu ── */
416
+ .action-menu { position: relative; display: inline-block; }
417
+ .action-menu-btn {
418
+ background: none;
419
+ border: 1px solid var(--border);
420
+ border-radius: var(--radius-sm);
421
+ padding: 4px 8px;
422
+ cursor: pointer;
423
+ color: var(--text-muted);
424
+ display: flex;
425
+ align-items: center;
426
+ gap: 3px;
427
+ font-size: 12px;
428
+ transition: all .12s;
429
+ }
430
+ .action-menu-btn:hover { background: var(--surface2); color: var(--text-soft); border-color: var(--border); }
431
+ .action-dropdown {
432
+ position: absolute;
433
+ right: 0;
434
+ top: calc(100% + 4px);
435
+ background: var(--surface);
436
+ border: 1px solid var(--border);
437
+ border-radius: var(--radius);
438
+ box-shadow: var(--shadow-lg);
439
+ min-width: 140px;
440
+ z-index: 50;
441
+ overflow: hidden;
442
+ display: none;
443
+ }
444
+ .action-dropdown.open { display: block; }
445
+ .action-dropdown a,
446
+ .action-dropdown button {
447
+ display: flex;
448
+ align-items: center;
449
+ gap: 8px;
450
+ padding: 8px 14px;
451
+ font-size: 13px;
452
+ color: var(--text-soft);
453
+ text-decoration: none;
454
+ background: none;
455
+ border: none;
456
+ width: 100%;
457
+ text-align: left;
458
+ cursor: pointer;
459
+ font-family: inherit;
460
+ transition: background .1s;
461
+ }
462
+ .action-dropdown a:hover,
463
+ .action-dropdown button:hover { background: var(--surface2); }
464
+ .action-dropdown .sep { height: 1px; background: var(--border-soft); margin: 3px 0; }
465
+ .action-dropdown .danger { color: var(--danger); }
187
466
 
188
- /* ── Badges ──────────────────────────────────────────────────── */
467
+ /* ════════════════════════════════════════
468
+ BADGES
469
+ ════════════════════════════════════════ */
189
470
  .badge {
190
- display: inline-flex; align-items: center;
191
- padding: 2px 8px; border-radius: 99px;
192
- font-size: 11px; font-weight: 600; white-space: nowrap;
193
- }
194
- .badge-blue { background: #1e3a5f; color: #60a5fa; }
195
- .badge-red { background: #3b1212; color: #f87171; }
196
- .badge-green { background: #0d2818; color: #4ade80; }
197
- .badge-yellow { background: #3b2800; color: #fbbf24; }
198
- .badge-purple { background: #1e1f3a; color: var(--primary-h); }
199
- .badge-gray { background: var(--surface2); color: var(--text-muted); }
200
-
201
- /* ── Buttons ─────────────────────────────────────────────────── */
471
+ display: inline-flex;
472
+ align-items: center;
473
+ gap: 4px;
474
+ padding: 2px 8px;
475
+ border-radius: 99px;
476
+ font-size: 11.5px;
477
+ font-weight: 600;
478
+ white-space: nowrap;
479
+ border: 1px solid transparent;
480
+ }
481
+ .badge-blue { background: #eff6ff; color: #1d4ed8; border-color: #bfdbfe; }
482
+ .badge-red { background: #fef2f2; color: #b91c1c; border-color: #fecaca; }
483
+ .badge-green { background: #f0fdf4; color: #15803d; border-color: #bbf7d0; }
484
+ .badge-yellow { background: #fffbeb; color: #b45309; border-color: #fde68a; }
485
+ .badge-purple { background: #faf5ff; color: #7e22ce; border-color: #e9d5ff; }
486
+ .badge-gray { background: #f9fafb; color: #6b7280; border-color: #e5e7eb; }
487
+ .badge-orange { background: #fff7ed; color: #c2410c; border-color: #fed7aa; }
488
+
489
+ /* ════════════════════════════════════════
490
+ BUTTONS
491
+ ════════════════════════════════════════ */
202
492
  .btn {
203
- display: inline-flex; align-items: center; gap: 6px;
204
- padding: 7px 14px; border-radius: var(--radius-sm);
205
- font-size: 13px; font-weight: 500; cursor: pointer;
206
- border: none; transition: all .15s; text-decoration: none;
207
- font-family: inherit; line-height: 1;
208
- }
209
- .btn-primary { background: var(--primary); color: #fff; }
210
- .btn-primary:hover { background: var(--primary-h); }
211
- .btn-ghost { background: transparent; color: var(--text-soft); border: 1px solid var(--border); }
493
+ display: inline-flex;
494
+ align-items: center;
495
+ gap: 6px;
496
+ padding: 7px 14px;
497
+ border-radius: var(--radius-sm);
498
+ font-size: 13px;
499
+ font-weight: 500;
500
+ cursor: pointer;
501
+ border: 1px solid transparent;
502
+ transition: all .12s;
503
+ text-decoration: none;
504
+ font-family: inherit;
505
+ line-height: 1;
506
+ white-space: nowrap;
507
+ }
508
+ .btn-primary {
509
+ background: var(--primary);
510
+ color: #fff;
511
+ border-color: var(--primary);
512
+ box-shadow: 0 1px 3px rgba(37,99,235,.3);
513
+ }
514
+ .btn-primary:hover { background: var(--primary-h); border-color: var(--primary-h); }
515
+ .btn-ghost {
516
+ background: transparent;
517
+ color: var(--text-soft);
518
+ border-color: var(--border);
519
+ }
212
520
  .btn-ghost:hover { background: var(--surface2); color: var(--text); }
213
- .btn-danger { background: transparent; color: var(--danger); border: 1px solid var(--border); }
214
- .btn-danger:hover { background: var(--danger); color: #fff; border-color: var(--danger); }
215
- .btn-sm { padding: 4px 10px; font-size: 12px; }
216
- .btn-icon { padding: 6px 8px; }
217
-
218
- /* ── Forms ───────────────────────────────────────────────────── */
219
- .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
220
- .form-group { display: flex; flex-direction: column; gap: 6px; }
221
- .form-group.full { grid-column: 1 / -1; }
222
- .form-label { font-size: 12px; font-weight: 500; color: var(--text-soft); }
521
+ .btn-danger {
522
+ background: transparent;
523
+ color: var(--danger);
524
+ border-color: var(--border);
525
+ }
526
+ .btn-danger:hover { background: var(--danger-bg); border-color: var(--danger-border); }
527
+ .btn-success {
528
+ background: var(--success);
529
+ color: #fff;
530
+ border-color: var(--success);
531
+ }
532
+ .btn-success:hover { opacity: .9; }
533
+ .btn-sm { padding: 5px 10px; font-size: 12px; }
534
+ .btn-xs { padding: 3px 8px; font-size: 11px; }
535
+ .btn-icon { padding: 6px; }
536
+ .btn:disabled { opacity: .5; cursor: not-allowed; pointer-events: none; }
537
+
538
+ /* ════════════════════════════════════════
539
+ FORMS
540
+ ════════════════════════════════════════ */
541
+ .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; }
542
+ .form-group { display: flex; flex-direction: column; gap: 5px; }
543
+ .form-group.full { grid-column: 1 / -1; }
544
+ .form-group.w-third { grid-column: span 1; }
545
+ .form-label { font-size: 12.5px; font-weight: 500; color: var(--text-soft); }
223
546
  .form-label .required { color: var(--danger); margin-left: 2px; }
224
547
  .form-control {
225
- background: var(--surface2); border: 1px solid var(--border);
226
- color: var(--text); border-radius: var(--radius-sm);
227
- padding: 8px 12px; font-size: 13px; width: 100%;
228
- outline: none; font-family: inherit; transition: border .15s;
548
+ background: var(--surface);
549
+ border: 1px solid var(--border);
550
+ color: var(--text);
551
+ border-radius: var(--radius-sm);
552
+ padding: 8px 11px;
553
+ font-size: 13.5px;
554
+ width: 100%;
555
+ outline: none;
556
+ font-family: inherit;
557
+ transition: border .12s, box-shadow .12s;
558
+ }
559
+ .form-control:hover { border-color: #c4c9d4; }
560
+ .form-control:focus {
561
+ border-color: var(--primary);
562
+ box-shadow: 0 0 0 3px var(--primary-soft);
229
563
  }
230
- .form-control:focus { border-color: var(--primary); }
231
- .form-help { font-size: 11px; color: var(--text-muted); }
232
- .form-error { font-size: 11px; color: var(--danger); }
233
- textarea.form-control { resize: vertical; min-height: 80px; }
564
+ .form-control.error {
565
+ border-color: var(--danger);
566
+ box-shadow: 0 0 0 3px var(--danger-bg);
567
+ }
568
+ .form-control::placeholder { color: var(--text-xmuted); }
569
+ select.form-control { cursor: pointer; }
570
+ textarea.form-control { resize: vertical; min-height: 90px; }
571
+ .form-help { font-size: 11.5px; color: var(--text-muted); }
572
+ .form-error { font-size: 11.5px; color: var(--danger); display: flex; align-items: center; gap: 4px; }
234
573
 
235
- /* ── Search ──────────────────────────────────────────────────── */
574
+ /* ── Checkbox / radio ── */
575
+ .check-group { display: flex; align-items: center; gap: 8px; }
576
+ .check-input {
577
+ width: 16px; height: 16px;
578
+ accent-color: var(--primary);
579
+ cursor: pointer;
580
+ }
581
+ .check-label { font-size: 13.5px; color: var(--text-soft); cursor: pointer; }
582
+
583
+ /* ── Search ── */
236
584
  .search-wrap { position: relative; }
237
- .search-wrap .search-icon {
238
- position: absolute; left: 10px; top: 50%;
239
- transform: translateY(-50%); color: var(--text-muted);
240
- font-size: 13px; pointer-events: none;
585
+ .search-icon-inner {
586
+ position: absolute;
587
+ left: 10px;
588
+ top: 50%;
589
+ transform: translateY(-50%);
590
+ color: var(--text-muted);
591
+ pointer-events: none;
592
+ font-size: 14px;
593
+ display: flex;
594
+ }
595
+ .search-input { padding-left: 34px !important; width: 220px; }
596
+
597
+ /* ════════════════════════════════════════
598
+ TOOLBAR (list page)
599
+ ════════════════════════════════════════ */
600
+ .toolbar {
601
+ display: flex;
602
+ align-items: center;
603
+ gap: 8px;
604
+ padding: 12px 18px;
605
+ border-bottom: 1px solid var(--border-soft);
606
+ flex-wrap: wrap;
607
+ }
608
+ .toolbar-left { display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; }
609
+ .toolbar-right { display: flex; align-items: center; gap: 8px; }
610
+
611
+ /* ── Bulk action bar ── */
612
+ .bulk-bar {
613
+ display: none;
614
+ align-items: center;
615
+ gap: 10px;
616
+ padding: 10px 18px;
617
+ background: var(--primary-soft);
618
+ border-bottom: 1px solid var(--primary-dim);
619
+ font-size: 13px;
241
620
  }
242
- .search-input { padding-left: 32px !important; width: 240px; }
621
+ .bulk-bar.visible { display: flex; }
622
+ .bulk-count { font-weight: 600; color: var(--primary); }
243
623
 
244
- /* ── Pagination ──────────────────────────────────────────────── */
624
+ /* ════════════════════════════════════════
625
+ FILTER PANEL
626
+ ════════════════════════════════════════ */
627
+ .filter-row {
628
+ display: flex;
629
+ align-items: flex-end;
630
+ gap: 10px;
631
+ padding: 12px 18px;
632
+ border-bottom: 1px solid var(--border-soft);
633
+ flex-wrap: wrap;
634
+ background: var(--surface2);
635
+ }
636
+ .filter-group { display: flex; flex-direction: column; gap: 4px; }
637
+ .filter-label { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .4px; color: var(--text-muted); }
638
+ .filter-control {
639
+ background: var(--surface);
640
+ border: 1px solid var(--border);
641
+ color: var(--text);
642
+ border-radius: var(--radius-sm);
643
+ padding: 6px 10px;
644
+ font-size: 12.5px;
645
+ outline: none;
646
+ font-family: inherit;
647
+ transition: border .12s;
648
+ }
649
+ .filter-control:focus { border-color: var(--primary); }
650
+
651
+ /* ════════════════════════════════════════
652
+ PAGINATION
653
+ ════════════════════════════════════════ */
245
654
  .pagination {
246
- display: flex; align-items: center; gap: 4px;
247
- padding: 14px 16px; border-top: 1px solid var(--border);
655
+ display: flex;
656
+ align-items: center;
657
+ gap: 3px;
658
+ padding: 12px 18px;
659
+ border-top: 1px solid var(--border-soft);
660
+ flex-wrap: wrap;
248
661
  }
249
662
  .page-info { font-size: 12px; color: var(--text-muted); margin-left: auto; }
250
663
  .page-btn {
251
- width: 30px; height: 30px;
252
- display: inline-flex; align-items: center; justify-content: center;
253
- border-radius: var(--radius-sm); border: 1px solid var(--border);
254
- background: transparent; color: var(--text-soft);
255
- cursor: pointer; font-size: 12px; font-family: inherit;
256
- transition: all .15s;
664
+ min-width: 30px;
665
+ height: 30px;
666
+ padding: 0 6px;
667
+ display: inline-flex;
668
+ align-items: center;
669
+ justify-content: center;
670
+ border-radius: var(--radius-sm);
671
+ border: 1px solid var(--border);
672
+ background: transparent;
673
+ color: var(--text-soft);
674
+ cursor: pointer;
675
+ font-size: 12px;
676
+ font-family: inherit;
677
+ transition: all .12s;
678
+ text-decoration: none;
257
679
  }
258
- .page-btn:hover:not(:disabled) { background: var(--surface2); color: var(--text); }
680
+ .page-btn:hover:not(:disabled) { background: var(--surface2); color: var(--text); border-color: #c4c9d4; }
259
681
  .page-btn.active { background: var(--primary); border-color: var(--primary); color: #fff; }
260
- .page-btn:disabled { opacity: 0.3; cursor: not-allowed; }
682
+ .page-btn:disabled { opacity: .35; cursor: not-allowed; }
683
+ .page-ellipsis { color: var(--text-muted); font-size: 12px; padding: 0 3px; }
261
684
 
262
- /* ── Modal ───────────────────────────────────────────────────── */
685
+ /* ════════════════════════════════════════
686
+ MODAL
687
+ ════════════════════════════════════════ */
263
688
  .modal-overlay {
264
- position: fixed; inset: 0;
265
- background: rgba(0,0,0,.7);
266
- display: flex; align-items: center; justify-content: center;
267
- z-index: 200; padding: 24px;
268
- opacity: 0; pointer-events: none; transition: opacity .2s;
689
+ position: fixed;
690
+ inset: 0;
691
+ background: rgba(17,24,39,.5);
692
+ display: flex;
693
+ align-items: center;
694
+ justify-content: center;
695
+ z-index: 200;
696
+ padding: 24px;
697
+ opacity: 0;
698
+ pointer-events: none;
699
+ transition: opacity .18s;
700
+ backdrop-filter: blur(2px);
269
701
  }
270
702
  .modal-overlay.open { opacity: 1; pointer-events: all; }
271
703
  .modal {
272
704
  background: var(--surface);
273
705
  border: 1px solid var(--border);
274
- border-radius: 12px; width: 100%; max-width: 560px;
275
- max-height: 90vh; overflow-y: auto;
276
- transform: translateY(12px); transition: transform .2s;
277
- box-shadow: var(--shadow);
706
+ border-radius: var(--radius-lg);
707
+ width: 100%;
708
+ max-width: 520px;
709
+ max-height: 90vh;
710
+ overflow-y: auto;
711
+ transform: translateY(10px) scale(.99);
712
+ transition: transform .2s;
713
+ box-shadow: var(--shadow-lg);
278
714
  }
279
- .modal-overlay.open .modal { transform: translateY(0); }
715
+ .modal-overlay.open .modal { transform: translateY(0) scale(1); }
716
+ .modal-sm { max-width: 400px; }
717
+ .modal-lg { max-width: 700px; }
280
718
  .modal-header {
281
- padding: 20px 24px; border-bottom: 1px solid var(--border);
282
- display: flex; justify-content: space-between; align-items: center;
283
- position: sticky; top: 0; background: var(--surface); z-index: 1;
719
+ padding: 18px 22px;
720
+ border-bottom: 1px solid var(--border-soft);
721
+ display: flex;
722
+ justify-content: space-between;
723
+ align-items: center;
724
+ position: sticky;
725
+ top: 0;
726
+ background: var(--surface);
727
+ z-index: 1;
284
728
  }
285
- .modal-title { font-size: 15px; font-weight: 600; }
286
- .modal-body { padding: 20px 24px; }
729
+ .modal-title { font-size: 15px; font-weight: 600; color: var(--text); }
730
+ .modal-body { padding: 20px 22px; }
287
731
  .modal-footer {
288
- padding: 16px 24px; border-top: 1px solid var(--border);
289
- display: flex; justify-content: flex-end; gap: 8px;
290
- position: sticky; bottom: 0; background: var(--surface);
732
+ padding: 14px 22px;
733
+ border-top: 1px solid var(--border-soft);
734
+ display: flex;
735
+ justify-content: flex-end;
736
+ gap: 8px;
737
+ position: sticky;
738
+ bottom: 0;
739
+ background: var(--surface);
291
740
  }
292
741
  .close-btn {
293
- background: none; border: none; color: var(--text-muted);
294
- font-size: 20px; cursor: pointer; line-height: 1; padding: 2px;
742
+ background: none;
743
+ border: none;
744
+ color: var(--text-muted);
745
+ cursor: pointer;
746
+ padding: 4px;
747
+ line-height: 1;
748
+ border-radius: 4px;
749
+ display: flex;
750
+ align-items: center;
751
+ transition: background .1s, color .1s;
295
752
  }
296
- .close-btn:hover { color: var(--text); }
753
+ .close-btn:hover { background: var(--surface3); color: var(--text); }
297
754
 
298
- /* ── Empty states ────────────────────────────────────────────── */
755
+ /* ════════════════════════════════════════
756
+ EMPTY STATE
757
+ ════════════════════════════════════════ */
299
758
  .empty-state {
300
- text-align: center; padding: 60px 20px;
759
+ text-align: center;
760
+ padding: 56px 24px;
301
761
  color: var(--text-muted);
302
762
  }
303
- .empty-icon { font-size: 48px; margin-bottom: 16px; opacity: .5; }
304
- .empty-title { font-size: 16px; font-weight: 600; color: var(--text-soft); margin-bottom: 8px; }
305
- .empty-desc { font-size: 13px; max-width: 320px; margin: 0 auto 20px; }
306
-
307
- /* ── Boolean cells ───────────────────────────────────────────── */
308
- .bool-yes { color: var(--success); font-size: 16px; }
309
- .bool-no { color: var(--danger); font-size: 16px; }
310
- .cell-muted { color: var(--text-muted); font-style: italic; }
763
+ .empty-icon {
764
+ width: 52px;
765
+ height: 52px;
766
+ background: var(--surface3);
767
+ border-radius: 14px;
768
+ display: flex;
769
+ align-items: center;
770
+ justify-content: center;
771
+ margin: 0 auto 16px;
772
+ color: var(--text-xmuted);
773
+ }
774
+ .empty-title { font-size: 15px; font-weight: 600; color: var(--text-soft); margin-bottom: 7px; }
775
+ .empty-desc { font-size: 13px; max-width: 320px; margin: 0 auto 20px; line-height: 1.6; }
311
776
 
312
- /* ── Misc ────────────────────────────────────────────────────── */
313
- .flex { display: flex; }
314
- .items-center { align-items: center; }
315
- .gap-2 { gap: 8px; }
316
- .gap-3 { gap: 12px; }
317
- .ml-auto { margin-left: auto; }
318
- .text-muted { color: var(--text-muted); }
319
- .text-sm { font-size: 12px; }
320
- .fw-600 { font-weight: 600; }
321
- .mb-4 { margin-bottom: 16px; }
322
- .mb-6 { margin-bottom: 24px; }
777
+ /* ════════════════════════════════════════
778
+ CELL TYPES
779
+ ════════════════════════════════════════ */
780
+ .bool-yes {
781
+ display: inline-flex; align-items: center; justify-content: center;
782
+ width: 20px; height: 20px; border-radius: 99px;
783
+ background: var(--success-bg); color: var(--success);
784
+ }
785
+ .bool-no {
786
+ display: inline-flex; align-items: center; justify-content: center;
787
+ width: 20px; height: 20px; border-radius: 99px;
788
+ background: var(--danger-bg); color: var(--danger);
789
+ }
790
+ .cell-muted { color: var(--text-xmuted); font-style: italic; }
791
+ .cell-mono { font-family: 'DM Mono', monospace; font-size: 12px; }
792
+ .cell-image { width: 34px; height: 34px; border-radius: 7px; object-fit: cover; border: 1px solid var(--border); }
323
793
 
324
- /* ── Toast notifications ─────────────────────────────────────── */
794
+ /* ════════════════════════════════════════
795
+ TOAST
796
+ ════════════════════════════════════════ */
325
797
  #toast-root {
326
- position: fixed; bottom: 24px; right: 24px;
327
- display: flex; flex-direction: column; gap: 8px;
328
- z-index: 500; pointer-events: none;
798
+ position: fixed;
799
+ bottom: 22px;
800
+ right: 22px;
801
+ display: flex;
802
+ flex-direction: column;
803
+ gap: 8px;
804
+ z-index: 500;
805
+ pointer-events: none;
329
806
  }
330
807
  .toast {
331
- background: var(--surface2); border: 1px solid var(--border);
332
- border-radius: var(--radius-sm); padding: 12px 16px;
333
- font-size: 13px; max-width: 320px;
334
- box-shadow: var(--shadow); pointer-events: all;
335
- display: flex; align-items: center; gap: 8px;
336
- animation: slideIn .2s ease;
337
- }
338
- @keyframes slideIn { from { transform: translateX(20px); opacity: 0; } }
339
- .toast-success { border-left: 3px solid var(--success); }
340
- .toast-error { border-left: 3px solid var(--danger); }
341
-
342
- /* ── Responsive ──────────────────────────────────────────────── */
808
+ background: var(--text);
809
+ color: #fff;
810
+ border-radius: var(--radius);
811
+ padding: 11px 16px;
812
+ font-size: 13px;
813
+ max-width: 320px;
814
+ box-shadow: var(--shadow-lg);
815
+ pointer-events: all;
816
+ display: flex;
817
+ align-items: center;
818
+ gap: 9px;
819
+ animation: toastIn .2s ease;
820
+ }
821
+ .toast-success .toast-dot { color: #4ade80; }
822
+ .toast-error .toast-dot { color: #f87171; }
823
+ @keyframes toastIn { from { transform: translateX(16px); opacity: 0; } }
824
+
825
+ /* ════════════════════════════════════════
826
+ UTILITIES
827
+ ════════════════════════════════════════ */
828
+ .flex { display: flex; }
829
+ .flex-col { flex-direction: column; }
830
+ .items-center { align-items: center; }
831
+ .justify-between { justify-content: space-between; }
832
+ .gap-1 { gap: 4px; }
833
+ .gap-2 { gap: 8px; }
834
+ .gap-3 { gap: 12px; }
835
+ .gap-4 { gap: 16px; }
836
+ .ml-auto { margin-left: auto; }
837
+ .text-muted { color: var(--text-muted); }
838
+ .text-sm { font-size: 12px; }
839
+ .text-xs { font-size: 11px; }
840
+ .fw-500 { font-weight: 500; }
841
+ .fw-600 { font-weight: 600; }
842
+ .mb-3 { margin-bottom: 12px; }
843
+ .mb-4 { margin-bottom: 16px; }
844
+ .mb-5 { margin-bottom: 20px; }
845
+ .mb-6 { margin-bottom: 24px; }
846
+ .truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
847
+
848
+ /* ════════════════════════════════════════
849
+ RESPONSIVE
850
+ ════════════════════════════════════════ */
343
851
  @media (max-width: 768px) {
344
852
  #sidebar { display: none; }
345
853
  .form-grid { grid-template-columns: 1fr; }
854
+ .search-input { width: 160px; }
346
855
  }
347
856
  </style>
348
857
  {% block head %}{% endblock %}
349
858
  </head>
350
859
  <body>
351
860
 
352
- {# ── Sidebar ── #}
861
+ {# ══════════════════════════════════════
862
+ SVG ICON DEFINITIONS (hidden sprite)
863
+ ══════════════════════════════════════ #}
864
+ <svg xmlns="http://www.w3.org/2000/svg" style="display:none">
865
+ <symbol id="ic-home" viewBox="0 0 24 24"><path d="M3 9.5L12 3l9 6.5V20a1 1 0 01-1 1H5a1 1 0 01-1-1V9.5z"/><path d="M9 21V12h6v9"/></symbol>
866
+ <symbol id="ic-table" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 3v18"/></symbol>
867
+ <symbol id="ic-users" viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87M16 3.13a4 4 0 010 7.75"/></symbol>
868
+ <symbol id="ic-file" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8z"/><path d="M14 2v6h6M16 13H8M16 17H8M10 9H8"/></symbol>
869
+ <symbol id="ic-tag" viewBox="0 0 24 24"><path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z"/><circle cx="7" cy="7" r="1.5" fill="currentColor" stroke="none"/></symbol>
870
+ <symbol id="ic-settings" viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></symbol>
871
+ <symbol id="ic-plus" viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></symbol>
872
+ <symbol id="ic-edit" viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></symbol>
873
+ <symbol id="ic-trash" viewBox="0 0 24 24"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 011-1h4a1 1 0 011 1v2"/></symbol>
874
+ <symbol id="ic-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></symbol>
875
+ <symbol id="ic-filter" viewBox="0 0 24 24"><polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/></symbol>
876
+ <symbol id="ic-chevron-left" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></symbol>
877
+ <symbol id="ic-chevron-right" viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></symbol>
878
+ <symbol id="ic-chevron-down" viewBox="0 0 24 24"><polyline points="6 9 12 15 18 9"/></symbol>
879
+ <symbol id="ic-x" viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></symbol>
880
+ <symbol id="ic-check" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></symbol>
881
+ <symbol id="ic-alert-circle" viewBox="0 0 24 24"><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"/></symbol>
882
+ <symbol id="ic-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></symbol>
883
+ <symbol id="ic-eye" viewBox="0 0 24 24"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></symbol>
884
+ <symbol id="ic-download" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></symbol>
885
+ <symbol id="ic-upload" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></symbol>
886
+ <symbol id="ic-refresh" viewBox="0 0 24 24"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0114.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0020.49 15"/></symbol>
887
+ <symbol id="ic-more-vertical" viewBox="0 0 24 24"><circle cx="12" cy="5" r="1" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="1" fill="currentColor" stroke="none"/><circle cx="12" cy="19" r="1" fill="currentColor" stroke="none"/></symbol>
888
+ <symbol id="ic-grid" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></symbol>
889
+ <symbol id="ic-list" viewBox="0 0 24 24"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></symbol>
890
+ <symbol id="ic-save" viewBox="0 0 24 24"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></symbol>
891
+ <symbol id="ic-arrow-left" viewBox="0 0 24 24"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></symbol>
892
+ <symbol id="ic-database" viewBox="0 0 24 24"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/><path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/></symbol>
893
+ <symbol id="ic-activity" viewBox="0 0 24 24"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></symbol>
894
+ <symbol id="ic-log-out" viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></symbol>
895
+ </svg>
896
+
897
+ {# ══════════════════════════════════════
898
+ SIDEBAR
899
+ ══════════════════════════════════════ #}
353
900
  <nav id="sidebar">
354
901
  <div class="sidebar-brand">
355
- <div class="brand-icon">⚡</div>
902
+ <div class="brand-logo">
903
+ <span class="icon icon-18">{% include "icons/database.svg" ignore missing %}<svg viewBox="0 0 24 24"><use href="#ic-database"/></svg></span>
904
+ </div>
356
905
  <div class="brand-text">
357
906
  <div class="brand-name">{{ adminTitle }}</div>
358
907
  <div class="brand-sub">Admin Panel</div>
@@ -362,7 +911,10 @@
362
911
  <div class="nav-section">
363
912
  <div class="nav-label">Overview</div>
364
913
  <a href="{{ adminPrefix }}/" class="nav-item {% if activePage == 'dashboard' %}active{% endif %}">
365
- <span class="nav-icon">🏠</span> Dashboard
914
+ <span class="nav-icon icon icon-16">
915
+ <svg viewBox="0 0 24 24"><use href="#ic-grid"/></svg>
916
+ </span>
917
+ Dashboard
366
918
  </a>
367
919
  </div>
368
920
 
@@ -371,7 +923,9 @@
371
923
  <div class="nav-label">Resources</div>
372
924
  {% for resource in resources %}
373
925
  <a href="{{ adminPrefix }}/{{ resource.slug }}" class="nav-item {% if activeResource == resource.slug %}active{% endif %}">
374
- <span class="nav-icon">{{ resource.icon }}</span>
926
+ <span class="nav-icon icon icon-16">
927
+ <svg viewBox="0 0 24 24"><use href="#ic-table"/></svg>
928
+ </span>
375
929
  {{ resource.label }}
376
930
  </a>
377
931
  {% endfor %}
@@ -379,11 +933,16 @@
379
933
  {% endif %}
380
934
 
381
935
  <div class="sidebar-footer">
382
- <div class="sidebar-version">Millas v0.1.1</div>
936
+ <div class="sidebar-version flex items-center gap-1">
937
+ <span class="icon icon-12" style="color:var(--text-xmuted)"><svg viewBox="0 0 24 24"><use href="#ic-activity"/></svg></span>
938
+ Millas v0.1.2
939
+ </div>
383
940
  </div>
384
941
  </nav>
385
942
 
386
- {# ── Main ── #}
943
+ {# ══════════════════════════════════════
944
+ MAIN
945
+ ══════════════════════════════════════ #}
387
946
  <div id="main">
388
947
  <header id="topbar">
389
948
  <span class="topbar-title">{% block topbar_title %}{{ pageTitle }}{% endblock %}</span>
@@ -394,10 +953,22 @@
394
953
 
395
954
  <div id="content">
396
955
  {% if flash.success %}
397
- <div class="alert alert-success">✓ {{ flash.success }}</div>
956
+ <div class="alert alert-success" id="flash-alert">
957
+ <span class="icon icon-16"><svg viewBox="0 0 24 24"><use href="#ic-check"/></svg></span>
958
+ <span>{{ flash.success }}</span>
959
+ <button class="alert-close" onclick="this.closest('.alert').remove()">
960
+ <span class="icon icon-14"><svg viewBox="0 0 24 24"><use href="#ic-x"/></svg></span>
961
+ </button>
962
+ </div>
398
963
  {% endif %}
399
964
  {% if flash.error %}
400
- <div class="alert alert-error">✕ {{ flash.error }}</div>
965
+ <div class="alert alert-error" id="flash-alert">
966
+ <span class="icon icon-16"><svg viewBox="0 0 24 24"><use href="#ic-alert-circle"/></svg></span>
967
+ <span>{{ flash.error }}</span>
968
+ <button class="alert-close" onclick="this.closest('.alert').remove()">
969
+ <span class="icon icon-14"><svg viewBox="0 0 24 24"><use href="#ic-x"/></svg></span>
970
+ </button>
971
+ </div>
401
972
  {% endif %}
402
973
 
403
974
  {% block content %}{% endblock %}
@@ -405,22 +976,30 @@
405
976
  </div>
406
977
 
407
978
  <div id="toast-root"></div>
408
- <div id="modal-root"></div>
409
979
 
410
980
  <script>
411
- // ── Toast utility ────────────────────────────────────────────
981
+ // ── Toast ────────────────────────────────────────────────────
412
982
  function toast(msg, type = 'success') {
983
+ const icons = {
984
+ success: '<polyline points="20 6 9 17 4 12"/>',
985
+ 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"/>',
986
+ };
413
987
  const el = document.createElement('div');
414
- el.className = 'toast toast-' + type;
415
- el.innerHTML = (type === 'success' ? '✓' : '✕') + ' ' + msg;
988
+ el.className = `toast toast-${type}`;
989
+ el.innerHTML = `
990
+ <span class="toast-dot icon icon-15" style="flex-shrink:0">
991
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
992
+ ${icons[type] || icons.success}
993
+ </svg>
994
+ </span>
995
+ <span>${msg}</span>`;
416
996
  document.getElementById('toast-root').appendChild(el);
417
- setTimeout(() => el.remove(), 3500);
997
+ setTimeout(() => el.style.opacity = '0', 3000);
998
+ setTimeout(() => el.remove(), 3300);
418
999
  }
419
1000
 
420
- // ── Modal utility ────────────────────────────────────────────
421
- function openModal(id) {
422
- document.getElementById(id)?.classList.add('open');
423
- }
1001
+ // ── Modal ────────────────────────────────────────────────────
1002
+ function openModal(id) { document.getElementById(id)?.classList.add('open'); }
424
1003
  function closeModal(id) {
425
1004
  if (id) document.getElementById(id)?.classList.remove('open');
426
1005
  else document.querySelectorAll('.modal-overlay').forEach(m => m.classList.remove('open'));
@@ -432,36 +1011,78 @@
432
1011
  if (e.key === 'Escape') closeModal();
433
1012
  });
434
1013
 
435
- // ── Delete confirmation ───────────────────────────────────────
1014
+ // ── Action dropdowns ────────────────────────────────────────
1015
+ document.addEventListener('click', e => {
1016
+ const btn = e.target.closest('.action-menu-btn');
1017
+ if (btn) {
1018
+ e.stopPropagation();
1019
+ const menu = btn.nextElementSibling;
1020
+ const isOpen = menu.classList.contains('open');
1021
+ document.querySelectorAll('.action-dropdown.open').forEach(d => d.classList.remove('open'));
1022
+ if (!isOpen) menu.classList.add('open');
1023
+ return;
1024
+ }
1025
+ if (!e.target.closest('.action-dropdown')) {
1026
+ document.querySelectorAll('.action-dropdown.open').forEach(d => d.classList.remove('open'));
1027
+ }
1028
+ });
1029
+
1030
+ // ── Delete confirmation ──────────────────────────────────────
436
1031
  function confirmDelete(url, label) {
437
1032
  const overlay = document.createElement('div');
438
1033
  overlay.className = 'modal-overlay open';
439
- overlay.innerHTML = \`
440
- <div class="modal" style="max-width:420px">
1034
+ overlay.innerHTML = `
1035
+ <div class="modal modal-sm">
441
1036
  <div class="modal-header">
442
- <span class="modal-title">Confirm Delete</span>
443
- <button class="close-btn" onclick="this.closest('.modal-overlay').remove()">×</button>
1037
+ <span class="modal-title flex items-center gap-2">
1038
+ <span class="icon icon-16" style="color:var(--danger)">
1039
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1040
+ <polyline points="3 6 5 6 21 6"/>
1041
+ <path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6M10 11v6M14 11v6"/>
1042
+ <path d="M9 6V4a1 1 0 011-1h4a1 1 0 011 1v2"/>
1043
+ </svg>
1044
+ </span>
1045
+ Delete ${label}
1046
+ </span>
1047
+ <button class="close-btn" onclick="this.closest('.modal-overlay').remove()">
1048
+ <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>
1049
+ </button>
444
1050
  </div>
445
1051
  <div class="modal-body">
446
- <p style="color:var(--text-soft)">
447
- Are you sure you want to delete <strong>\${label}</strong>?
1052
+ <p style="color:var(--text-soft);line-height:1.6">
1053
+ Are you sure you want to delete <strong style="color:var(--text)">${label}</strong>?
448
1054
  This action cannot be undone.
449
1055
  </p>
450
1056
  </div>
451
1057
  <div class="modal-footer">
452
1058
  <button class="btn btn-ghost" onclick="this.closest('.modal-overlay').remove()">Cancel</button>
453
- <button class="btn btn-danger" onclick="submitDelete('\${url}')">Delete</button>
1059
+ <button class="btn btn-danger" onclick="submitDelete('${url}')">
1060
+ <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>
1061
+ Delete
1062
+ </button>
454
1063
  </div>
455
- </div>\`;
1064
+ </div>`;
1065
+ overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
456
1066
  document.body.appendChild(overlay);
457
1067
  }
458
1068
  function submitDelete(url) {
459
1069
  const form = document.createElement('form');
460
- form.method = 'POST'; form.action = url;
1070
+ form.method = 'POST';
1071
+ form.action = url;
461
1072
  form.innerHTML = '<input name="_method" value="DELETE">';
462
1073
  document.body.appendChild(form);
463
1074
  form.submit();
464
1075
  }
1076
+
1077
+ // ── Auto-dismiss flash after 5s ──────────────────────────────
1078
+ const flashAlert = document.getElementById('flash-alert');
1079
+ if (flashAlert) {
1080
+ setTimeout(() => {
1081
+ flashAlert.style.transition = 'opacity .4s';
1082
+ flashAlert.style.opacity = '0';
1083
+ setTimeout(() => flashAlert.remove(), 400);
1084
+ }, 5000);
1085
+ }
465
1086
  </script>
466
1087
  {% block scripts %}{% endblock %}
467
1088
  </body>