millas 0.2.3 → 0.2.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.
- package/package.json +1 -1
- package/src/admin/ActivityLog.js +95 -0
- package/src/admin/Admin.js +336 -20
- package/src/admin/resources/AdminResource.js +140 -82
- package/src/admin/views/layouts/base.njk +96 -1
- package/src/admin/views/pages/dashboard.njk +273 -61
- package/src/admin/views/pages/list.njk +19 -0
- package/src/admin/views/pages/search.njk +139 -0
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{% extends "layouts/base.njk" %}
|
|
2
2
|
{% block title %}Dashboard{% endblock %}
|
|
3
3
|
|
|
4
|
+
{% block head %}
|
|
5
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js" defer></script>
|
|
6
|
+
{% endblock %}
|
|
7
|
+
|
|
4
8
|
{% block content %}
|
|
5
9
|
<div class="breadcrumb">
|
|
6
10
|
<span class="icon icon-13" style="color:var(--text-xmuted)"><svg viewBox="0 0 24 24"><use href="#ic-grid"/></svg></span>
|
|
@@ -8,26 +12,8 @@
|
|
|
8
12
|
<span class="breadcrumb-current">Dashboard</span>
|
|
9
13
|
</div>
|
|
10
14
|
|
|
11
|
-
{# ── Stat cards ── #}
|
|
12
|
-
{% if resources | length %}
|
|
13
|
-
<div class="stats-grid">
|
|
14
|
-
{% for resource in resources %}
|
|
15
|
-
<a href="{{ adminPrefix }}/{{ resource.slug }}" class="stat-card" style="cursor:pointer">
|
|
16
|
-
<div class="stat-icon-wrap">
|
|
17
|
-
<span class="icon icon-18">
|
|
18
|
-
<svg viewBox="0 0 24 24"><use href="#ic-table"/></svg>
|
|
19
|
-
</span>
|
|
20
|
-
</div>
|
|
21
|
-
<div class="stat-label">{{ resource.label }}</div>
|
|
22
|
-
<div class="stat-value">{{ resource.count if resource.count is defined else '—' }}</div>
|
|
23
|
-
<div class="stat-sub">Total records</div>
|
|
24
|
-
</a>
|
|
25
|
-
{% endfor %}
|
|
26
|
-
</div>
|
|
27
|
-
{% endif %}
|
|
28
|
-
|
|
29
|
-
{# ── No resources welcome state ── #}
|
|
30
15
|
{% if not resources | length %}
|
|
16
|
+
{# ── Empty / onboarding state ── #}
|
|
31
17
|
<div class="card">
|
|
32
18
|
<div class="card-body">
|
|
33
19
|
<div class="empty-state">
|
|
@@ -35,12 +21,8 @@
|
|
|
35
21
|
<span class="icon icon-24"><svg viewBox="0 0 24 24"><use href="#ic-database"/></svg></span>
|
|
36
22
|
</div>
|
|
37
23
|
<div class="empty-title">Welcome to Millas Admin</div>
|
|
38
|
-
<div class="empty-desc">
|
|
39
|
-
|
|
40
|
-
Add this to your <code style="font-family:'DM Mono',monospace;background:var(--surface3);padding:1px 5px;border-radius:3px;font-size:12px">AppServiceProvider.boot()</code>:
|
|
41
|
-
</div>
|
|
42
|
-
<pre style="background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius);padding:16px;text-align:left;font-family:'DM Mono',monospace;font-size:12px;margin:0 auto;max-width:480px;overflow-x:auto;color:var(--text-soft)">const { Admin } = resolveMillas();
|
|
43
|
-
Admin.register(UserResource);
|
|
24
|
+
<div class="empty-desc">Register your models to start managing your data.</div>
|
|
25
|
+
<pre style="background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius);padding:16px;text-align:left;font-family:'DM Mono',monospace;font-size:12px;margin:0 auto;max-width:480px;overflow-x:auto;color:var(--text-soft)">Admin.register(UserResource);
|
|
44
26
|
Admin.mount(expressApp);</pre>
|
|
45
27
|
</div>
|
|
46
28
|
</div>
|
|
@@ -48,43 +30,273 @@ Admin.mount(expressApp);</pre>
|
|
|
48
30
|
|
|
49
31
|
{% else %}
|
|
50
32
|
|
|
51
|
-
{# ──
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<div class="
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
{%
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
</
|
|
73
|
-
</
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
33
|
+
{# ── Two-column layout: main + activity sidebar ── #}
|
|
34
|
+
<div class="dash-layout">
|
|
35
|
+
|
|
36
|
+
{# ════════ LEFT COLUMN ════════ #}
|
|
37
|
+
<div class="dash-main">
|
|
38
|
+
|
|
39
|
+
{# ── Stat cards ── #}
|
|
40
|
+
<div class="stats-grid mb-5">
|
|
41
|
+
{% for resource in resources %}
|
|
42
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}" class="stat-card">
|
|
43
|
+
<div class="flex items-center justify-between mb-3">
|
|
44
|
+
<div class="stat-icon-wrap">
|
|
45
|
+
<span class="icon icon-18"><svg viewBox="0 0 24 24"><use href="#ic-{{ resource.icon or 'table' }}"/></svg></span>
|
|
46
|
+
</div>
|
|
47
|
+
{% if resource.recentCount > 0 %}
|
|
48
|
+
<span class="badge badge-green" style="font-size:10.5px">
|
|
49
|
+
+{{ resource.recentCount }} this week
|
|
50
|
+
</span>
|
|
51
|
+
{% endif %}
|
|
52
|
+
</div>
|
|
53
|
+
<div class="stat-value">{{ resource.count if resource.count is defined else '—' }}</div>
|
|
54
|
+
<div class="stat-label" style="margin-top:4px">{{ resource.label }}</div>
|
|
55
|
+
</a>
|
|
56
|
+
{% endfor %}
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{# ── Activity summary bar ── #}
|
|
60
|
+
{% if activityTotals %}
|
|
61
|
+
<div class="card mb-5" style="overflow:visible">
|
|
62
|
+
<div class="card-body" style="padding:16px 20px">
|
|
63
|
+
<div class="flex items-center gap-4 flex-wrap">
|
|
64
|
+
<span class="text-sm fw-600" style="color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px">Today's activity</span>
|
|
65
|
+
<div class="flex items-center gap-3" style="flex:1;flex-wrap:wrap">
|
|
66
|
+
<div class="activity-pill activity-create">
|
|
67
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-plus"/></svg></span>
|
|
68
|
+
{{ activityTotals.create }} created
|
|
69
|
+
</div>
|
|
70
|
+
<div class="activity-pill activity-update">
|
|
71
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-edit"/></svg></span>
|
|
72
|
+
{{ activityTotals.update }} updated
|
|
73
|
+
</div>
|
|
74
|
+
<div class="activity-pill activity-delete">
|
|
75
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-trash"/></svg></span>
|
|
76
|
+
{{ activityTotals.delete }} deleted
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
{% endif %}
|
|
83
|
+
|
|
84
|
+
{# ── Recent records per resource ── #}
|
|
85
|
+
{% for resource in resources %}
|
|
86
|
+
{% if resource.recent | length %}
|
|
87
|
+
<div class="card mb-5">
|
|
88
|
+
<div class="card-header">
|
|
89
|
+
<span class="card-title">
|
|
90
|
+
<span class="icon icon-15" style="color:var(--primary)">
|
|
91
|
+
<svg viewBox="0 0 24 24"><use href="#ic-{{ resource.icon or 'table' }}"/></svg>
|
|
92
|
+
</span>
|
|
93
|
+
Recent {{ resource.label }}
|
|
94
|
+
</span>
|
|
95
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}" class="btn btn-ghost btn-sm">
|
|
96
|
+
View all
|
|
97
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-chevron-right"/></svg></span>
|
|
98
|
+
</a>
|
|
99
|
+
</div>
|
|
100
|
+
<div class="table-wrap">
|
|
101
|
+
<table>
|
|
102
|
+
<thead>
|
|
103
|
+
<tr>
|
|
104
|
+
{% for field in resource.listFields %}
|
|
105
|
+
<th>{{ field.label }}</th>
|
|
106
|
+
{% endfor %}
|
|
107
|
+
<th class="col-actions">Actions</th>
|
|
108
|
+
</tr>
|
|
109
|
+
</thead>
|
|
110
|
+
<tbody>
|
|
111
|
+
{% for row in resource.recent %}
|
|
112
|
+
<tr>
|
|
113
|
+
{% for field in resource.listFields %}
|
|
114
|
+
<td {% if loop.first %}class="td-primary"{% endif %}>{{ row[field.name] | adminCell(field) | safe }}</td>
|
|
115
|
+
{% endfor %}
|
|
116
|
+
<td class="col-actions">
|
|
117
|
+
<div class="flex items-center gap-1" style="justify-content:flex-end">
|
|
118
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}/{{ row.id }}" class="btn btn-ghost btn-xs">
|
|
119
|
+
<span class="icon icon-12"><svg viewBox="0 0 24 24"><use href="#ic-eye"/></svg></span>
|
|
120
|
+
</a>
|
|
121
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}/{{ row.id }}/edit" class="btn btn-ghost btn-xs">
|
|
122
|
+
<span class="icon icon-12"><svg viewBox="0 0 24 24"><use href="#ic-edit"/></svg></span>
|
|
123
|
+
</a>
|
|
124
|
+
</div>
|
|
125
|
+
</td>
|
|
126
|
+
</tr>
|
|
127
|
+
{% endfor %}
|
|
128
|
+
</tbody>
|
|
129
|
+
</table>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
{% endif %}
|
|
133
|
+
{% endfor %}
|
|
134
|
+
|
|
135
|
+
</div>{# end dash-main #}
|
|
136
|
+
|
|
137
|
+
{# ════════ RIGHT COLUMN — Activity feed ════════ #}
|
|
138
|
+
<div class="dash-sidebar">
|
|
139
|
+
|
|
140
|
+
{# ── Quick actions ── #}
|
|
141
|
+
<div class="card mb-4">
|
|
142
|
+
<div class="card-header">
|
|
143
|
+
<span class="card-title">
|
|
144
|
+
<span class="icon icon-14" style="color:var(--primary)"><svg viewBox="0 0 24 24"><use href="#ic-activity"/></svg></span>
|
|
145
|
+
Quick Actions
|
|
146
|
+
</span>
|
|
147
|
+
</div>
|
|
148
|
+
<div style="padding:10px 12px;display:flex;flex-direction:column;gap:4px">
|
|
149
|
+
{% for resource in resources %}
|
|
150
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}/create" class="quick-action-btn">
|
|
151
|
+
<span class="icon icon-13" style="color:var(--primary)"><svg viewBox="0 0 24 24"><use href="#ic-plus"/></svg></span>
|
|
152
|
+
New {{ resource.singular }}
|
|
153
|
+
<span class="icon icon-12 ml-auto" style="color:var(--text-xmuted)"><svg viewBox="0 0 24 24"><use href="#ic-chevron-right"/></svg></span>
|
|
154
|
+
</a>
|
|
81
155
|
{% endfor %}
|
|
82
|
-
</
|
|
83
|
-
</
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
|
|
159
|
+
{# ── Activity feed ── #}
|
|
160
|
+
<div class="card">
|
|
161
|
+
<div class="card-header">
|
|
162
|
+
<span class="card-title">
|
|
163
|
+
<span class="icon icon-14" style="color:var(--primary)"><svg viewBox="0 0 24 24"><use href="#ic-list"/></svg></span>
|
|
164
|
+
Recent Activity
|
|
165
|
+
</span>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
{% if activity | length %}
|
|
169
|
+
<div style="divide-y:var(--border)">
|
|
170
|
+
{% for entry in activity %}
|
|
171
|
+
<div class="feed-entry">
|
|
172
|
+
<div class="feed-dot feed-dot-{{ entry.action }}">
|
|
173
|
+
{% if entry.action == 'create' %}
|
|
174
|
+
<svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
|
175
|
+
{% elif entry.action == 'update' %}
|
|
176
|
+
<svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><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"/></svg>
|
|
177
|
+
{% else %}
|
|
178
|
+
<svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2.5" 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>
|
|
179
|
+
{% endif %}
|
|
180
|
+
</div>
|
|
181
|
+
<div class="feed-body">
|
|
182
|
+
<div class="feed-text">
|
|
183
|
+
<span class="feed-action feed-action-{{ entry.action }}">{{ entry.action | capitalize }}</span>
|
|
184
|
+
<span class="feed-label">{{ entry.label }}</span>
|
|
185
|
+
<span class="feed-resource">in {{ entry.resource }}</span>
|
|
186
|
+
</div>
|
|
187
|
+
<div class="feed-time" title="{{ entry.at }}">{{ entry.at | relativeTime }}</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
{% endfor %}
|
|
191
|
+
</div>
|
|
192
|
+
{% else %}
|
|
193
|
+
<div class="empty-state" style="padding:32px 20px">
|
|
194
|
+
<div class="empty-icon" style="width:36px;height:36px;margin:0 auto 12px">
|
|
195
|
+
<span class="icon icon-18"><svg viewBox="0 0 24 24"><use href="#ic-activity"/></svg></span>
|
|
196
|
+
</div>
|
|
197
|
+
<div class="empty-title" style="font-size:13px">No activity yet</div>
|
|
198
|
+
<div class="empty-desc" style="font-size:12px">Actions will appear here as you manage data.</div>
|
|
199
|
+
</div>
|
|
200
|
+
{% endif %}
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
{# ── Keyboard shortcuts cheatsheet ── #}
|
|
204
|
+
<div class="card mt-4" style="background:var(--surface2)">
|
|
205
|
+
<div class="card-header" style="background:transparent">
|
|
206
|
+
<span class="card-title text-sm" style="color:var(--text-muted)">
|
|
207
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-settings"/></svg></span>
|
|
208
|
+
Keyboard Shortcuts
|
|
209
|
+
</span>
|
|
210
|
+
</div>
|
|
211
|
+
<div style="padding:10px 16px;display:flex;flex-direction:column;gap:7px">
|
|
212
|
+
{% set shortcuts = [
|
|
213
|
+
['/', 'Focus search'],
|
|
214
|
+
['N', 'New record (on list)'],
|
|
215
|
+
['G + D', 'Go to Dashboard'],
|
|
216
|
+
['G + 1–9', 'Jump to resource'],
|
|
217
|
+
['Esc', 'Close modal / blur']
|
|
218
|
+
] %}
|
|
219
|
+
{% for sc in shortcuts %}
|
|
220
|
+
<div class="shortcut-row">
|
|
221
|
+
<span class="kbd">{{ sc[0] }}</span>
|
|
222
|
+
<span class="shortcut-desc">{{ sc[1] }}</span>
|
|
223
|
+
</div>
|
|
224
|
+
{% endfor %}
|
|
225
|
+
</div>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
</div>{# end dash-sidebar #}
|
|
229
|
+
</div>{# end dash-layout #}
|
|
88
230
|
|
|
89
231
|
{% endif %}
|
|
232
|
+
|
|
233
|
+
<style>
|
|
234
|
+
.dash-layout {
|
|
235
|
+
display: grid;
|
|
236
|
+
grid-template-columns: 1fr 260px;
|
|
237
|
+
gap: 20px;
|
|
238
|
+
align-items: start;
|
|
239
|
+
}
|
|
240
|
+
@media (max-width: 900px) {
|
|
241
|
+
.dash-layout { grid-template-columns: 1fr; }
|
|
242
|
+
.dash-sidebar { display: none; }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* ── Activity pills ── */
|
|
246
|
+
.activity-pill {
|
|
247
|
+
display: inline-flex; align-items: center; gap: 5px;
|
|
248
|
+
padding: 4px 10px; border-radius: 99px;
|
|
249
|
+
font-size: 12px; font-weight: 500;
|
|
250
|
+
border: 1px solid transparent;
|
|
251
|
+
}
|
|
252
|
+
.activity-create { background: var(--success-bg); color: var(--success); border-color: var(--success-border); }
|
|
253
|
+
.activity-update { background: var(--info-bg); color: var(--info); border-color: var(--info-border); }
|
|
254
|
+
.activity-delete { background: var(--danger-bg); color: var(--danger); border-color: var(--danger-border); }
|
|
255
|
+
|
|
256
|
+
/* ── Activity feed ── */
|
|
257
|
+
.feed-entry {
|
|
258
|
+
display: flex; gap: 10px; padding: 10px 16px;
|
|
259
|
+
border-bottom: 1px solid var(--border-soft);
|
|
260
|
+
}
|
|
261
|
+
.feed-entry:last-child { border-bottom: none; }
|
|
262
|
+
.feed-dot {
|
|
263
|
+
width: 20px; height: 20px; border-radius: 99px; flex-shrink: 0;
|
|
264
|
+
display: flex; align-items: center; justify-content: center;
|
|
265
|
+
margin-top: 1px;
|
|
266
|
+
}
|
|
267
|
+
.feed-dot-create { background: var(--success-bg); color: var(--success); }
|
|
268
|
+
.feed-dot-update { background: var(--info-bg); color: var(--info); }
|
|
269
|
+
.feed-dot-delete { background: var(--danger-bg); color: var(--danger); }
|
|
270
|
+
.feed-body { flex: 1; min-width: 0; }
|
|
271
|
+
.feed-text { font-size: 12.5px; color: var(--text-soft); line-height: 1.4; }
|
|
272
|
+
.feed-action { font-weight: 600; }
|
|
273
|
+
.feed-action-create { color: var(--success); }
|
|
274
|
+
.feed-action-update { color: var(--info); }
|
|
275
|
+
.feed-action-delete { color: var(--danger); }
|
|
276
|
+
.feed-label { color: var(--text); font-weight: 500; margin: 0 3px; }
|
|
277
|
+
.feed-resource { color: var(--text-muted); }
|
|
278
|
+
.feed-time { font-size: 11px; color: var(--text-xmuted); margin-top: 2px; }
|
|
279
|
+
|
|
280
|
+
/* ── Quick actions ── */
|
|
281
|
+
.quick-action-btn {
|
|
282
|
+
display: flex; align-items: center; gap: 8px;
|
|
283
|
+
padding: 8px 10px; border-radius: var(--radius-sm);
|
|
284
|
+
font-size: 13px; color: var(--text-soft); text-decoration: none;
|
|
285
|
+
transition: background .1s;
|
|
286
|
+
}
|
|
287
|
+
.quick-action-btn:hover { background: var(--surface2); color: var(--text); }
|
|
288
|
+
|
|
289
|
+
/* ── Shortcuts ── */
|
|
290
|
+
.shortcut-row { display: flex; align-items: center; gap: 10px; }
|
|
291
|
+
.kbd {
|
|
292
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
293
|
+
background: var(--surface); border: 1px solid var(--border);
|
|
294
|
+
border-bottom-width: 2px; border-radius: 4px;
|
|
295
|
+
padding: 2px 6px; font-family: 'DM Mono', monospace;
|
|
296
|
+
font-size: 11px; color: var(--text-soft);
|
|
297
|
+
white-space: nowrap; min-width: 28px; text-align: center;
|
|
298
|
+
box-shadow: 0 1px 0 var(--border);
|
|
299
|
+
}
|
|
300
|
+
.shortcut-desc { font-size: 12px; color: var(--text-muted); }
|
|
301
|
+
</style>
|
|
90
302
|
{% endblock %}
|
|
@@ -73,6 +73,25 @@
|
|
|
73
73
|
{% endif %}
|
|
74
74
|
</button>
|
|
75
75
|
{% endif %}
|
|
76
|
+
|
|
77
|
+
{# Export dropdown #}
|
|
78
|
+
<div class="action-menu">
|
|
79
|
+
<button class="btn btn-ghost btn-sm action-menu-btn">
|
|
80
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-download"/></svg></span>
|
|
81
|
+
Export
|
|
82
|
+
<span class="icon icon-12"><svg viewBox="0 0 24 24"><use href="#ic-chevron-down"/></svg></span>
|
|
83
|
+
</button>
|
|
84
|
+
<div class="action-dropdown" style="right:0;min-width:160px">
|
|
85
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}/export.csv?search={{ search }}&sort={{ sort }}&order={{ order }}{% for key, val in activeFilters %}&filter[{{ key }}]={{ val }}{% endfor %}">
|
|
86
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-download"/></svg></span>
|
|
87
|
+
Export CSV
|
|
88
|
+
</a>
|
|
89
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}/export.json?search={{ search }}&sort={{ sort }}&order={{ order }}{% for key, val in activeFilters %}&filter[{{ key }}]={{ val }}{% endfor %}">
|
|
90
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-download"/></svg></span>
|
|
91
|
+
Export JSON
|
|
92
|
+
</a>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
76
95
|
</div>
|
|
77
96
|
</div>
|
|
78
97
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
{% extends "layouts/base.njk" %}
|
|
2
|
+
{% block title %}Search{% endblock %}
|
|
3
|
+
{% block topbar_title %}
|
|
4
|
+
<span class="icon icon-16" style="color:var(--text-muted)">
|
|
5
|
+
<svg viewBox="0 0 24 24"><use href="#ic-search"/></svg>
|
|
6
|
+
</span>
|
|
7
|
+
{% if query %}Search: {{ query }}{% else %}Search{% endif %}
|
|
8
|
+
{% endblock %}
|
|
9
|
+
|
|
10
|
+
{% block content %}
|
|
11
|
+
<div class="breadcrumb">
|
|
12
|
+
<a href="{{ adminPrefix }}/">
|
|
13
|
+
<span class="icon icon-12"><svg viewBox="0 0 24 24"><use href="#ic-grid"/></svg></span>
|
|
14
|
+
</a>
|
|
15
|
+
<span class="breadcrumb-sep">›</span>
|
|
16
|
+
<span class="breadcrumb-current">Search</span>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
{# ── Search box (large, prominent) ── #}
|
|
20
|
+
<div class="card mb-5" style="max-width:600px">
|
|
21
|
+
<div class="card-body" style="padding:20px">
|
|
22
|
+
<form action="{{ adminPrefix }}/search" method="GET">
|
|
23
|
+
<div class="flex items-center gap-3">
|
|
24
|
+
<div class="search-wrap" style="flex:1">
|
|
25
|
+
<span class="search-icon-inner">
|
|
26
|
+
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
27
|
+
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
|
28
|
+
</svg>
|
|
29
|
+
</span>
|
|
30
|
+
<input
|
|
31
|
+
type="text"
|
|
32
|
+
name="q"
|
|
33
|
+
value="{{ query }}"
|
|
34
|
+
placeholder="Search across all resources…"
|
|
35
|
+
class="form-control search-input"
|
|
36
|
+
style="width:100%;font-size:14px;padding-top:10px;padding-bottom:10px"
|
|
37
|
+
autofocus
|
|
38
|
+
autocomplete="off">
|
|
39
|
+
</div>
|
|
40
|
+
<button type="submit" class="btn btn-primary">Search</button>
|
|
41
|
+
{% if query %}
|
|
42
|
+
<a href="{{ adminPrefix }}/search" class="btn btn-ghost">Clear</a>
|
|
43
|
+
{% endif %}
|
|
44
|
+
</div>
|
|
45
|
+
</form>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
{# ── No query ── #}
|
|
50
|
+
{% if not query %}
|
|
51
|
+
<div class="empty-state" style="padding:40px 20px">
|
|
52
|
+
<div class="empty-icon">
|
|
53
|
+
<span class="icon icon-22"><svg viewBox="0 0 24 24"><use href="#ic-search"/></svg></span>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="empty-title">Search your data</div>
|
|
56
|
+
<div class="empty-desc">Type in the box above to search across all registered resources simultaneously.</div>
|
|
57
|
+
<div class="flex items-center gap-2 mt-3" style="justify-content:center;flex-wrap:wrap">
|
|
58
|
+
{% for resource in resources %}
|
|
59
|
+
<a href="{{ adminPrefix }}/{{ resource.slug }}" class="btn btn-ghost btn-sm">
|
|
60
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-table"/></svg></span>
|
|
61
|
+
{{ resource.label }}
|
|
62
|
+
</a>
|
|
63
|
+
{% endfor %}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
{# ── No results ── #}
|
|
68
|
+
{% elif not results | length %}
|
|
69
|
+
<div class="empty-state" style="padding:40px 20px">
|
|
70
|
+
<div class="empty-icon">
|
|
71
|
+
<span class="icon icon-22"><svg viewBox="0 0 24 24"><use href="#ic-search"/></svg></span>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="empty-title">No results found</div>
|
|
74
|
+
<div class="empty-desc">
|
|
75
|
+
Nothing matched <strong>"{{ query }}"</strong>. Try a different search term, or make sure the resource has
|
|
76
|
+
<code style="font-family:'DM Mono',monospace;background:var(--surface3);padding:1px 5px;border-radius:3px;font-size:12px">static searchable</code>
|
|
77
|
+
columns defined.
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{# ── Results ── #}
|
|
82
|
+
{% else %}
|
|
83
|
+
<div class="text-sm text-muted mb-4">
|
|
84
|
+
Found <strong>{{ total }}</strong> result{{ 's' if total != 1 }} for "<strong>{{ query }}</strong>" across {{ results | length }} resource{{ 's' if results | length != 1 }}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
{% for group in results %}
|
|
88
|
+
<div class="card mb-5">
|
|
89
|
+
<div class="card-header">
|
|
90
|
+
<span class="card-title">
|
|
91
|
+
<span class="icon icon-15" style="color:var(--primary)">
|
|
92
|
+
<svg viewBox="0 0 24 24"><use href="#ic-{{ group.icon or 'table' }}"/></svg>
|
|
93
|
+
</span>
|
|
94
|
+
{{ group.label }}
|
|
95
|
+
<span class="badge badge-blue" style="font-size:10.5px">{{ group.total }} result{{ 's' if group.total != 1 }}</span>
|
|
96
|
+
</span>
|
|
97
|
+
{% if group.total > group.rows | length %}
|
|
98
|
+
<a href="{{ adminPrefix }}/{{ group.slug }}?search={{ query }}" class="btn btn-ghost btn-sm">
|
|
99
|
+
View all {{ group.total }}
|
|
100
|
+
<span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-chevron-right"/></svg></span>
|
|
101
|
+
</a>
|
|
102
|
+
{% endif %}
|
|
103
|
+
</div>
|
|
104
|
+
<div class="table-wrap">
|
|
105
|
+
<table>
|
|
106
|
+
<thead>
|
|
107
|
+
<tr>
|
|
108
|
+
{% for field in group.listFields %}
|
|
109
|
+
<th>{{ field.label }}</th>
|
|
110
|
+
{% endfor %}
|
|
111
|
+
<th class="col-actions">Actions</th>
|
|
112
|
+
</tr>
|
|
113
|
+
</thead>
|
|
114
|
+
<tbody>
|
|
115
|
+
{% for row in group.rows %}
|
|
116
|
+
<tr>
|
|
117
|
+
{% for field in group.listFields %}
|
|
118
|
+
<td {% if loop.first %}class="td-primary"{% endif %}>{{ row[field.name] | adminCell(field) | safe }}</td>
|
|
119
|
+
{% endfor %}
|
|
120
|
+
<td class="col-actions">
|
|
121
|
+
<div class="flex items-center gap-1" style="justify-content:flex-end">
|
|
122
|
+
<a href="{{ adminPrefix }}/{{ group.slug }}/{{ row.id }}" class="btn btn-ghost btn-xs">
|
|
123
|
+
<span class="icon icon-12"><svg viewBox="0 0 24 24"><use href="#ic-eye"/></svg></span>
|
|
124
|
+
</a>
|
|
125
|
+
<a href="{{ adminPrefix }}/{{ group.slug }}/{{ row.id }}/edit" class="btn btn-ghost btn-xs">
|
|
126
|
+
<span class="icon icon-12"><svg viewBox="0 0 24 24"><use href="#ic-edit"/></svg></span>
|
|
127
|
+
</a>
|
|
128
|
+
</div>
|
|
129
|
+
</td>
|
|
130
|
+
</tr>
|
|
131
|
+
{% endfor %}
|
|
132
|
+
</tbody>
|
|
133
|
+
</table>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
{% endfor %}
|
|
137
|
+
|
|
138
|
+
{% endif %}
|
|
139
|
+
{% endblock %}
|