millas 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,468 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{% block title %}{{ pageTitle }}{% endblock %} — {{ adminTitle }}</title>
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9
+ :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);
28
+ }
29
+ body {
30
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
31
+ background: var(--bg);
32
+ color: var(--text);
33
+ display: flex;
34
+ height: 100vh;
35
+ overflow: hidden;
36
+ font-size: 14px;
37
+ line-height: 1.5;
38
+ }
39
+
40
+ /* ── Sidebar ─────────────────────────────────────────────────── */
41
+ #sidebar {
42
+ width: 256px; min-width: 256px;
43
+ background: var(--surface);
44
+ border-right: 1px solid var(--border);
45
+ display: flex; flex-direction: column;
46
+ overflow-y: auto; overflow-x: hidden;
47
+ }
48
+ .sidebar-brand {
49
+ padding: 20px 20px 16px;
50
+ border-bottom: 1px solid var(--border);
51
+ display: flex; align-items: center; gap: 10px;
52
+ }
53
+ .brand-icon {
54
+ width: 32px; height: 32px;
55
+ background: linear-gradient(135deg, var(--primary), #a855f7);
56
+ border-radius: 8px;
57
+ display: flex; align-items: center; justify-content: center;
58
+ font-size: 16px; flex-shrink: 0;
59
+ }
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); }
63
+
64
+ .nav-section { padding: 12px 12px 4px; }
65
+ .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;
69
+ }
70
+ .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; }
81
+ .nav-count {
82
+ margin-left: auto; font-size: 11px;
83
+ background: var(--surface3); color: var(--text-muted);
84
+ padding: 1px 6px; border-radius: 99px;
85
+ }
86
+
87
+ .sidebar-footer {
88
+ margin-top: auto;
89
+ padding: 16px;
90
+ border-top: 1px solid var(--border);
91
+ }
92
+ .sidebar-version { font-size: 11px; color: var(--text-muted); text-align: center; }
93
+
94
+ /* ── Main ────────────────────────────────────────────────────── */
95
+ #main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
96
+
97
+ #topbar {
98
+ background: var(--surface);
99
+ border-bottom: 1px solid var(--border);
100
+ padding: 0 24px; height: 56px;
101
+ display: flex; align-items: center;
102
+ gap: 12px; flex-shrink: 0;
103
+ }
104
+ .topbar-title { font-size: 15px; font-weight: 600; flex: 1; }
105
+ .topbar-actions { display: flex; gap: 8px; align-items: center; }
106
+
107
+ #content { flex: 1; overflow-y: auto; padding: 24px; }
108
+
109
+ /* ── Breadcrumb ──────────────────────────────────────────────── */
110
+ .breadcrumb {
111
+ display: flex; align-items: center; gap: 6px;
112
+ font-size: 12px; color: var(--text-muted);
113
+ margin-bottom: 20px;
114
+ }
115
+ .breadcrumb a { color: var(--text-muted); text-decoration: none; }
116
+ .breadcrumb a:hover { color: var(--text); }
117
+ .breadcrumb-sep { color: var(--border); }
118
+
119
+ /* ── Alert ───────────────────────────────────────────────────── */
120
+ .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;
124
+ }
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; }
129
+
130
+ /* ── Cards ───────────────────────────────────────────────────── */
131
+ .card {
132
+ background: var(--surface);
133
+ border: 1px solid var(--border);
134
+ border-radius: var(--radius);
135
+ overflow: hidden;
136
+ }
137
+ .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;
142
+ }
143
+ .card-title { font-size: 14px; font-weight: 600; }
144
+ .card-body { padding: 20px; }
145
+
146
+ /* ── Stat Cards ──────────────────────────────────────────────── */
147
+ .stats-grid {
148
+ display: grid;
149
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
150
+ gap: 16px; margin-bottom: 24px;
151
+ }
152
+ .stat-card {
153
+ background: var(--surface);
154
+ border: 1px solid var(--border);
155
+ border-radius: var(--radius);
156
+ padding: 20px;
157
+ display: flex; flex-direction: column; gap: 8px;
158
+ }
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); }
163
+
164
+ /* ── Table ───────────────────────────────────────────────────── */
165
+ .table-wrap { overflow-x: auto; }
166
+ table { width: 100%; border-collapse: collapse; }
167
+ 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);
172
+ border-bottom: 1px solid var(--border);
173
+ white-space: nowrap;
174
+ }
175
+ 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); }
179
+ td {
180
+ padding: 12px 16px; font-size: 13px;
181
+ border-bottom: 1px solid var(--border);
182
+ vertical-align: middle;
183
+ }
184
+ tr:last-child td { border-bottom: none; }
185
+ tr:hover td { background: var(--surface2); }
186
+ .col-actions { width: 120px; text-align: right; }
187
+
188
+ /* ── Badges ──────────────────────────────────────────────────── */
189
+ .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 ─────────────────────────────────────────────────── */
202
+ .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); }
212
+ .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); }
223
+ .form-label .required { color: var(--danger); margin-left: 2px; }
224
+ .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;
229
+ }
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; }
234
+
235
+ /* ── Search ──────────────────────────────────────────────────── */
236
+ .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;
241
+ }
242
+ .search-input { padding-left: 32px !important; width: 240px; }
243
+
244
+ /* ── Pagination ──────────────────────────────────────────────── */
245
+ .pagination {
246
+ display: flex; align-items: center; gap: 4px;
247
+ padding: 14px 16px; border-top: 1px solid var(--border);
248
+ }
249
+ .page-info { font-size: 12px; color: var(--text-muted); margin-left: auto; }
250
+ .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;
257
+ }
258
+ .page-btn:hover:not(:disabled) { background: var(--surface2); color: var(--text); }
259
+ .page-btn.active { background: var(--primary); border-color: var(--primary); color: #fff; }
260
+ .page-btn:disabled { opacity: 0.3; cursor: not-allowed; }
261
+
262
+ /* ── Modal ───────────────────────────────────────────────────── */
263
+ .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;
269
+ }
270
+ .modal-overlay.open { opacity: 1; pointer-events: all; }
271
+ .modal {
272
+ background: var(--surface);
273
+ 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);
278
+ }
279
+ .modal-overlay.open .modal { transform: translateY(0); }
280
+ .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;
284
+ }
285
+ .modal-title { font-size: 15px; font-weight: 600; }
286
+ .modal-body { padding: 20px 24px; }
287
+ .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);
291
+ }
292
+ .close-btn {
293
+ background: none; border: none; color: var(--text-muted);
294
+ font-size: 20px; cursor: pointer; line-height: 1; padding: 2px;
295
+ }
296
+ .close-btn:hover { color: var(--text); }
297
+
298
+ /* ── Empty states ────────────────────────────────────────────── */
299
+ .empty-state {
300
+ text-align: center; padding: 60px 20px;
301
+ color: var(--text-muted);
302
+ }
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; }
311
+
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; }
323
+
324
+ /* ── Toast notifications ─────────────────────────────────────── */
325
+ #toast-root {
326
+ position: fixed; bottom: 24px; right: 24px;
327
+ display: flex; flex-direction: column; gap: 8px;
328
+ z-index: 500; pointer-events: none;
329
+ }
330
+ .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 ──────────────────────────────────────────────── */
343
+ @media (max-width: 768px) {
344
+ #sidebar { display: none; }
345
+ .form-grid { grid-template-columns: 1fr; }
346
+ }
347
+ </style>
348
+ {% block head %}{% endblock %}
349
+ </head>
350
+ <body>
351
+
352
+ {# ── Sidebar ── #}
353
+ <nav id="sidebar">
354
+ <div class="sidebar-brand">
355
+ <div class="brand-icon">⚡</div>
356
+ <div class="brand-text">
357
+ <div class="brand-name">{{ adminTitle }}</div>
358
+ <div class="brand-sub">Admin Panel</div>
359
+ </div>
360
+ </div>
361
+
362
+ <div class="nav-section">
363
+ <div class="nav-label">Overview</div>
364
+ <a href="{{ adminPrefix }}/" class="nav-item {% if activePage == 'dashboard' %}active{% endif %}">
365
+ <span class="nav-icon">🏠</span> Dashboard
366
+ </a>
367
+ </div>
368
+
369
+ {% if resources | length %}
370
+ <div class="nav-section">
371
+ <div class="nav-label">Resources</div>
372
+ {% for resource in resources %}
373
+ <a href="{{ adminPrefix }}/{{ resource.slug }}" class="nav-item {% if activeResource == resource.slug %}active{% endif %}">
374
+ <span class="nav-icon">{{ resource.icon }}</span>
375
+ {{ resource.label }}
376
+ </a>
377
+ {% endfor %}
378
+ </div>
379
+ {% endif %}
380
+
381
+ <div class="sidebar-footer">
382
+ <div class="sidebar-version">Millas v0.1.1</div>
383
+ </div>
384
+ </nav>
385
+
386
+ {# ── Main ── #}
387
+ <div id="main">
388
+ <header id="topbar">
389
+ <span class="topbar-title">{% block topbar_title %}{{ pageTitle }}{% endblock %}</span>
390
+ <div class="topbar-actions">
391
+ {% block topbar_actions %}{% endblock %}
392
+ </div>
393
+ </header>
394
+
395
+ <div id="content">
396
+ {% if flash.success %}
397
+ <div class="alert alert-success">✓ {{ flash.success }}</div>
398
+ {% endif %}
399
+ {% if flash.error %}
400
+ <div class="alert alert-error">✕ {{ flash.error }}</div>
401
+ {% endif %}
402
+
403
+ {% block content %}{% endblock %}
404
+ </div>
405
+ </div>
406
+
407
+ <div id="toast-root"></div>
408
+ <div id="modal-root"></div>
409
+
410
+ <script>
411
+ // ── Toast utility ────────────────────────────────────────────
412
+ function toast(msg, type = 'success') {
413
+ const el = document.createElement('div');
414
+ el.className = 'toast toast-' + type;
415
+ el.innerHTML = (type === 'success' ? '✓' : '✕') + ' ' + msg;
416
+ document.getElementById('toast-root').appendChild(el);
417
+ setTimeout(() => el.remove(), 3500);
418
+ }
419
+
420
+ // ── Modal utility ────────────────────────────────────────────
421
+ function openModal(id) {
422
+ document.getElementById(id)?.classList.add('open');
423
+ }
424
+ function closeModal(id) {
425
+ if (id) document.getElementById(id)?.classList.remove('open');
426
+ else document.querySelectorAll('.modal-overlay').forEach(m => m.classList.remove('open'));
427
+ }
428
+ document.addEventListener('click', e => {
429
+ if (e.target.classList.contains('modal-overlay')) closeModal();
430
+ });
431
+ document.addEventListener('keydown', e => {
432
+ if (e.key === 'Escape') closeModal();
433
+ });
434
+
435
+ // ── Delete confirmation ───────────────────────────────────────
436
+ function confirmDelete(url, label) {
437
+ const overlay = document.createElement('div');
438
+ overlay.className = 'modal-overlay open';
439
+ overlay.innerHTML = \`
440
+ <div class="modal" style="max-width:420px">
441
+ <div class="modal-header">
442
+ <span class="modal-title">Confirm Delete</span>
443
+ <button class="close-btn" onclick="this.closest('.modal-overlay').remove()">×</button>
444
+ </div>
445
+ <div class="modal-body">
446
+ <p style="color:var(--text-soft)">
447
+ Are you sure you want to delete <strong>\${label}</strong>?
448
+ This action cannot be undone.
449
+ </p>
450
+ </div>
451
+ <div class="modal-footer">
452
+ <button class="btn btn-ghost" onclick="this.closest('.modal-overlay').remove()">Cancel</button>
453
+ <button class="btn btn-danger" onclick="submitDelete('\${url}')">Delete</button>
454
+ </div>
455
+ </div>\`;
456
+ document.body.appendChild(overlay);
457
+ }
458
+ function submitDelete(url) {
459
+ const form = document.createElement('form');
460
+ form.method = 'POST'; form.action = url;
461
+ form.innerHTML = '<input name="_method" value="DELETE">';
462
+ document.body.appendChild(form);
463
+ form.submit();
464
+ }
465
+ </script>
466
+ {% block scripts %}{% endblock %}
467
+ </body>
468
+ </html>
@@ -0,0 +1,84 @@
1
+ {% extends "layouts/base.njk" %}
2
+
3
+ {% block title %}Dashboard{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="breadcrumb">
7
+ <span>🏠</span>
8
+ <span class="breadcrumb-sep">›</span>
9
+ <span>Dashboard</span>
10
+ </div>
11
+
12
+ {# ── Stat cards ── #}
13
+ <div class="stats-grid">
14
+ {% for resource in resources %}
15
+ <a href="{{ adminPrefix }}/{{ resource.slug }}" style="text-decoration:none">
16
+ <div class="stat-card" style="cursor:pointer;transition:border .15s" onmouseover="this.style.borderColor='var(--primary)'" onmouseout="this.style.borderColor='var(--border)'">
17
+ <div class="stat-icon">{{ resource.icon }}</div>
18
+ <div class="stat-label">{{ resource.label }}</div>
19
+ <div class="stat-value">{{ resource.count if resource.count is defined else '—' }}</div>
20
+ <div class="stat-sub">Total records</div>
21
+ </div>
22
+ </a>
23
+ {% else %}
24
+ <div class="stat-card">
25
+ <div class="stat-icon">📋</div>
26
+ <div class="stat-label">Resources</div>
27
+ <div class="stat-value">0</div>
28
+ <div class="stat-sub">None registered yet</div>
29
+ </div>
30
+ {% endfor %}
31
+ </div>
32
+
33
+ {# ── Quick start if no resources ── #}
34
+ {% if not resources | length %}
35
+ <div class="card">
36
+ <div class="card-body">
37
+ <div class="empty-state">
38
+ <div class="empty-icon">🚀</div>
39
+ <div class="empty-title">Welcome to Millas Admin</div>
40
+ <div class="empty-desc">
41
+ Register your models to start managing your data.
42
+ Add this to your <code>AppServiceProvider.boot()</code>:
43
+ </div>
44
+ <pre style="background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:16px;text-align:left;font-size:12px;margin:0 auto;max-width:480px;overflow-x:auto">
45
+ const { Admin, AdminResource } = resolveMillas();
46
+ Admin.register(UserResource);
47
+ Admin.mount(Route, expressApp);</pre>
48
+ </div>
49
+ </div>
50
+ </div>
51
+ {% else %}
52
+ {# ── Recent activity per resource ── #}
53
+ {% for resource in resources %}
54
+ {% if resource.recent | length %}
55
+ <div class="card mb-6">
56
+ <div class="card-header">
57
+ <span class="card-title">{{ resource.icon }} Recent {{ resource.label }}</span>
58
+ <a href="{{ adminPrefix }}/{{ resource.slug }}" class="btn btn-ghost btn-sm">View all →</a>
59
+ </div>
60
+ <div class="table-wrap">
61
+ <table>
62
+ <thead>
63
+ <tr>
64
+ {% for field in resource.listFields %}
65
+ <th>{{ field.label }}</th>
66
+ {% endfor %}
67
+ </tr>
68
+ </thead>
69
+ <tbody>
70
+ {% for row in resource.recent %}
71
+ <tr>
72
+ {% for field in resource.listFields %}
73
+ <td>{{ row[field.name] | adminCell(field) | safe }}</td>
74
+ {% endfor %}
75
+ </tr>
76
+ {% endfor %}
77
+ </tbody>
78
+ </table>
79
+ </div>
80
+ </div>
81
+ {% endif %}
82
+ {% endfor %}
83
+ {% endif %}
84
+ {% endblock %}