millas 0.2.4 → 0.2.6
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/Admin.js +241 -62
- package/src/admin/AdminAuth.js +281 -0
- package/src/admin/index.js +6 -1
- package/src/admin/resources/AdminResource.js +180 -29
- package/src/admin/views/layouts/base.njk +38 -1
- package/src/admin/views/pages/detail.njk +322 -0
- package/src/admin/views/pages/form.njk +571 -125
- package/src/admin/views/pages/list.njk +454 -0
- package/src/admin/views/pages/login.njk +354 -0
|
@@ -0,0 +1,354 @@
|
|
|
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>Sign In — {{ 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:opsz,wght@9..40,300;9..40,400;9..40,500;9..40,600;9..40,700&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
10
|
+
<style>
|
|
11
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
12
|
+
:root {
|
|
13
|
+
--bg: #f0f2f5;
|
|
14
|
+
--surface: #ffffff;
|
|
15
|
+
--border: #e3e6ec;
|
|
16
|
+
--border-soft: #edf0f5;
|
|
17
|
+
--primary: #2563eb;
|
|
18
|
+
--primary-h: #1d4ed8;
|
|
19
|
+
--primary-soft: #eff4ff;
|
|
20
|
+
--primary-dim: #dbeafe;
|
|
21
|
+
--text: #111827;
|
|
22
|
+
--text-soft: #374151;
|
|
23
|
+
--text-muted: #6b7280;
|
|
24
|
+
--text-xmuted: #9ca3af;
|
|
25
|
+
--danger: #dc2626;
|
|
26
|
+
--danger-bg: #fef2f2;
|
|
27
|
+
--danger-border:#fecaca;
|
|
28
|
+
--success: #16a34a;
|
|
29
|
+
--success-bg: #f0fdf4;
|
|
30
|
+
--success-border:#bbf7d0;
|
|
31
|
+
--radius: 8px;
|
|
32
|
+
--radius-sm: 6px;
|
|
33
|
+
--shadow: 0 4px 24px rgba(0,0,0,.08), 0 1px 4px rgba(0,0,0,.04);
|
|
34
|
+
}
|
|
35
|
+
body {
|
|
36
|
+
font-family: 'DM Sans', system-ui, sans-serif;
|
|
37
|
+
background: var(--bg);
|
|
38
|
+
color: var(--text);
|
|
39
|
+
min-height: 100vh;
|
|
40
|
+
display: flex;
|
|
41
|
+
flex-direction: column;
|
|
42
|
+
align-items: center;
|
|
43
|
+
justify-content: center;
|
|
44
|
+
padding: 24px;
|
|
45
|
+
-webkit-font-smoothing: antialiased;
|
|
46
|
+
font-size: 14px;
|
|
47
|
+
line-height: 1.5;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* ── Card ── */
|
|
51
|
+
.login-card {
|
|
52
|
+
background: var(--surface);
|
|
53
|
+
border: 1px solid var(--border);
|
|
54
|
+
border-radius: 14px;
|
|
55
|
+
box-shadow: var(--shadow);
|
|
56
|
+
width: 100%;
|
|
57
|
+
max-width: 400px;
|
|
58
|
+
overflow: hidden;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* ── Header ── */
|
|
62
|
+
.login-header {
|
|
63
|
+
padding: 32px 32px 28px;
|
|
64
|
+
border-bottom: 1px solid var(--border-soft);
|
|
65
|
+
text-align: center;
|
|
66
|
+
}
|
|
67
|
+
.login-logo {
|
|
68
|
+
width: 48px; height: 48px;
|
|
69
|
+
background: var(--primary);
|
|
70
|
+
border-radius: 13px;
|
|
71
|
+
display: flex; align-items: center; justify-content: center;
|
|
72
|
+
margin: 0 auto 16px;
|
|
73
|
+
box-shadow: 0 4px 12px rgba(37,99,235,.3);
|
|
74
|
+
}
|
|
75
|
+
.login-logo svg {
|
|
76
|
+
width: 24px; height: 24px;
|
|
77
|
+
fill: none; stroke: #fff;
|
|
78
|
+
stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round;
|
|
79
|
+
}
|
|
80
|
+
.login-title {
|
|
81
|
+
font-size: 20px;
|
|
82
|
+
font-weight: 700;
|
|
83
|
+
color: var(--text);
|
|
84
|
+
margin-bottom: 4px;
|
|
85
|
+
}
|
|
86
|
+
.login-subtitle {
|
|
87
|
+
font-size: 13px;
|
|
88
|
+
color: var(--text-muted);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* ── Body ── */
|
|
92
|
+
.login-body { padding: 28px 32px 32px; }
|
|
93
|
+
|
|
94
|
+
/* ── Alerts ── */
|
|
95
|
+
.alert {
|
|
96
|
+
display: flex; align-items: flex-start; gap: 9px;
|
|
97
|
+
padding: 11px 14px;
|
|
98
|
+
border-radius: var(--radius-sm);
|
|
99
|
+
font-size: 13px;
|
|
100
|
+
margin-bottom: 20px;
|
|
101
|
+
border: 1px solid transparent;
|
|
102
|
+
}
|
|
103
|
+
.alert svg { flex-shrink: 0; margin-top: 1px; }
|
|
104
|
+
.alert-error { background: var(--danger-bg); color: var(--danger); border-color: var(--danger-border); }
|
|
105
|
+
.alert-success { background: var(--success-bg); color: var(--success); border-color: var(--success-border); }
|
|
106
|
+
|
|
107
|
+
/* ── Form ── */
|
|
108
|
+
.form-group {
|
|
109
|
+
display: flex; flex-direction: column; gap: 5px;
|
|
110
|
+
margin-bottom: 16px;
|
|
111
|
+
}
|
|
112
|
+
.form-group:last-of-type { margin-bottom: 0; }
|
|
113
|
+
.form-label {
|
|
114
|
+
font-size: 12.5px;
|
|
115
|
+
font-weight: 500;
|
|
116
|
+
color: var(--text-soft);
|
|
117
|
+
}
|
|
118
|
+
.form-control {
|
|
119
|
+
background: var(--surface);
|
|
120
|
+
border: 1px solid var(--border);
|
|
121
|
+
color: var(--text);
|
|
122
|
+
border-radius: var(--radius-sm);
|
|
123
|
+
padding: 9px 12px;
|
|
124
|
+
font-size: 14px;
|
|
125
|
+
width: 100%;
|
|
126
|
+
outline: none;
|
|
127
|
+
font-family: inherit;
|
|
128
|
+
transition: border .12s, box-shadow .12s;
|
|
129
|
+
}
|
|
130
|
+
.form-control:hover { border-color: #c4c9d4; }
|
|
131
|
+
.form-control:focus {
|
|
132
|
+
border-color: var(--primary);
|
|
133
|
+
box-shadow: 0 0 0 3px var(--primary-soft);
|
|
134
|
+
}
|
|
135
|
+
.form-control.error {
|
|
136
|
+
border-color: var(--danger);
|
|
137
|
+
box-shadow: 0 0 0 3px var(--danger-bg);
|
|
138
|
+
}
|
|
139
|
+
.form-control::placeholder { color: var(--text-xmuted); }
|
|
140
|
+
|
|
141
|
+
/* ── Password field ── */
|
|
142
|
+
.pw-wrap { position: relative; }
|
|
143
|
+
.pw-toggle {
|
|
144
|
+
position: absolute; right: 10px; top: 50%;
|
|
145
|
+
transform: translateY(-50%);
|
|
146
|
+
background: none; border: none; cursor: pointer;
|
|
147
|
+
color: var(--text-muted); display: flex; align-items: center;
|
|
148
|
+
padding: 0;
|
|
149
|
+
}
|
|
150
|
+
.pw-toggle:hover { color: var(--text-soft); }
|
|
151
|
+
.pw-toggle svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
|
152
|
+
|
|
153
|
+
/* ── Remember me ── */
|
|
154
|
+
.remember-row {
|
|
155
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
156
|
+
margin: 16px 0 20px;
|
|
157
|
+
}
|
|
158
|
+
.check-group { display: flex; align-items: center; gap: 7px; cursor: pointer; }
|
|
159
|
+
.check-input { width: 15px; height: 15px; accent-color: var(--primary); cursor: pointer; }
|
|
160
|
+
.check-label { font-size: 13px; color: var(--text-soft); cursor: pointer; user-select: none; }
|
|
161
|
+
|
|
162
|
+
/* ── Submit ── */
|
|
163
|
+
.btn-login {
|
|
164
|
+
width: 100%;
|
|
165
|
+
background: var(--primary);
|
|
166
|
+
color: #fff;
|
|
167
|
+
border: none;
|
|
168
|
+
border-radius: var(--radius-sm);
|
|
169
|
+
padding: 10px 16px;
|
|
170
|
+
font-size: 14px;
|
|
171
|
+
font-weight: 600;
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
font-family: inherit;
|
|
174
|
+
display: flex; align-items: center; justify-content: center; gap: 8px;
|
|
175
|
+
transition: background .12s, transform .1s;
|
|
176
|
+
box-shadow: 0 1px 3px rgba(37,99,235,.3);
|
|
177
|
+
}
|
|
178
|
+
.btn-login:hover { background: var(--primary-h); }
|
|
179
|
+
.btn-login:active { transform: scale(.99); }
|
|
180
|
+
.btn-login:disabled { opacity: .6; cursor: not-allowed; }
|
|
181
|
+
.btn-login svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
|
182
|
+
|
|
183
|
+
/* ── Footer ── */
|
|
184
|
+
.login-footer {
|
|
185
|
+
padding: 16px 32px;
|
|
186
|
+
background: var(--bg);
|
|
187
|
+
border-top: 1px solid var(--border-soft);
|
|
188
|
+
text-align: center;
|
|
189
|
+
font-size: 12px;
|
|
190
|
+
color: var(--text-xmuted);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* ── Spinner ── */
|
|
194
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
195
|
+
.spin { animation: spin .8s linear infinite; }
|
|
196
|
+
|
|
197
|
+
/* ── Background decoration ── */
|
|
198
|
+
body::before {
|
|
199
|
+
content: '';
|
|
200
|
+
position: fixed; inset: 0;
|
|
201
|
+
background:
|
|
202
|
+
radial-gradient(ellipse 80% 50% at 20% 10%, rgba(37,99,235,.06) 0%, transparent 60%),
|
|
203
|
+
radial-gradient(ellipse 60% 40% at 80% 90%, rgba(99,102,241,.05) 0%, transparent 60%);
|
|
204
|
+
pointer-events: none;
|
|
205
|
+
}
|
|
206
|
+
</style>
|
|
207
|
+
</head>
|
|
208
|
+
<body>
|
|
209
|
+
|
|
210
|
+
<div class="login-card">
|
|
211
|
+
|
|
212
|
+
<div class="login-header">
|
|
213
|
+
<div class="login-logo">
|
|
214
|
+
<svg viewBox="0 0 24 24">
|
|
215
|
+
<ellipse cx="12" cy="5" rx="9" ry="3"/>
|
|
216
|
+
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/>
|
|
217
|
+
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>
|
|
218
|
+
</svg>
|
|
219
|
+
</div>
|
|
220
|
+
<div class="login-title">{{ adminTitle }}</div>
|
|
221
|
+
<div class="login-subtitle">Sign in to your admin panel</div>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<div class="login-body">
|
|
225
|
+
|
|
226
|
+
{# ── Success flash ── #}
|
|
227
|
+
{% if flash.success %}
|
|
228
|
+
<div class="alert alert-success">
|
|
229
|
+
<svg viewBox="0 0 24 24" width="15" height="15"><polyline points="20 6 9 17 4 12"/></svg>
|
|
230
|
+
{{ flash.success }}
|
|
231
|
+
</div>
|
|
232
|
+
{% endif %}
|
|
233
|
+
|
|
234
|
+
{# ── Error message ── #}
|
|
235
|
+
{% if error %}
|
|
236
|
+
<div class="alert alert-error" id="login-error">
|
|
237
|
+
<svg viewBox="0 0 24 24" width="15" height="15"><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"/></svg>
|
|
238
|
+
{{ error }}
|
|
239
|
+
</div>
|
|
240
|
+
{% endif %}
|
|
241
|
+
|
|
242
|
+
<form method="POST" action="{{ adminPrefix }}/login" id="login-form" novalidate>
|
|
243
|
+
{% if next %}
|
|
244
|
+
<input type="hidden" name="next" value="{{ next }}">
|
|
245
|
+
{% endif %}
|
|
246
|
+
|
|
247
|
+
<div class="form-group">
|
|
248
|
+
<label class="form-label" for="email">Email address</label>
|
|
249
|
+
<input
|
|
250
|
+
type="email"
|
|
251
|
+
id="email"
|
|
252
|
+
name="email"
|
|
253
|
+
value="{{ email or '' }}"
|
|
254
|
+
class="form-control{% if error %} error{% endif %}"
|
|
255
|
+
placeholder="admin@example.com"
|
|
256
|
+
autocomplete="email"
|
|
257
|
+
required
|
|
258
|
+
autofocus>
|
|
259
|
+
</div>
|
|
260
|
+
|
|
261
|
+
<div class="form-group">
|
|
262
|
+
<label class="form-label" for="password">Password</label>
|
|
263
|
+
<div class="pw-wrap">
|
|
264
|
+
<input
|
|
265
|
+
type="password"
|
|
266
|
+
id="password"
|
|
267
|
+
name="password"
|
|
268
|
+
class="form-control{% if error %} error{% endif %}"
|
|
269
|
+
placeholder="••••••••"
|
|
270
|
+
autocomplete="current-password"
|
|
271
|
+
required
|
|
272
|
+
style="padding-right: 40px">
|
|
273
|
+
<button type="button" class="pw-toggle" onclick="togglePw()" aria-label="Toggle password visibility">
|
|
274
|
+
<svg id="pw-icon" viewBox="0 0 24 24">
|
|
275
|
+
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
|
276
|
+
<circle cx="12" cy="12" r="3"/>
|
|
277
|
+
</svg>
|
|
278
|
+
</button>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
|
|
282
|
+
<div class="remember-row">
|
|
283
|
+
<label class="check-group" for="remember">
|
|
284
|
+
<input type="checkbox" id="remember" name="remember" class="check-input" value="on">
|
|
285
|
+
<span class="check-label">Remember me for 30 days</span>
|
|
286
|
+
</label>
|
|
287
|
+
</div>
|
|
288
|
+
|
|
289
|
+
<button type="submit" class="btn-login" id="login-btn">
|
|
290
|
+
<svg viewBox="0 0 24 24" id="login-icon">
|
|
291
|
+
<path d="M15 3h4a2 2 0 012 2v14a2 2 0 01-2 2h-4"/>
|
|
292
|
+
<polyline points="10 17 15 12 10 7"/>
|
|
293
|
+
<line x1="15" y1="12" x2="3" y2="12"/>
|
|
294
|
+
</svg>
|
|
295
|
+
Sign In
|
|
296
|
+
</button>
|
|
297
|
+
|
|
298
|
+
</form>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<div class="login-footer">
|
|
302
|
+
Millas Admin Panel · v0.1.2
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
</div>
|
|
306
|
+
|
|
307
|
+
<script>
|
|
308
|
+
function togglePw() {
|
|
309
|
+
const input = document.getElementById('password');
|
|
310
|
+
const icon = document.getElementById('pw-icon');
|
|
311
|
+
const isHidden = input.type === 'password';
|
|
312
|
+
input.type = isHidden ? 'text' : 'password';
|
|
313
|
+
icon.innerHTML = isHidden
|
|
314
|
+
? `<path d="M17.94 17.94A10.07 10.07 0 0112 20c-7 0-11-8-11-8a18.45 18.45 0 015.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0112 4c7 0 11 8 11 8a18.5 18.5 0 01-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/>`
|
|
315
|
+
: `<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"/>`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Submit loading state + basic client validation
|
|
319
|
+
document.getElementById('login-form').addEventListener('submit', function(e) {
|
|
320
|
+
const email = document.getElementById('email').value.trim();
|
|
321
|
+
const password = document.getElementById('password').value;
|
|
322
|
+
|
|
323
|
+
if (!email || !password) {
|
|
324
|
+
e.preventDefault();
|
|
325
|
+
const existing = document.getElementById('login-error');
|
|
326
|
+
const msg = !email ? 'Email is required.' : 'Password is required.';
|
|
327
|
+
if (existing) {
|
|
328
|
+
existing.querySelector('svg + *')
|
|
329
|
+
? (existing.lastChild.textContent = msg)
|
|
330
|
+
: (existing.innerHTML += ` ${msg}`);
|
|
331
|
+
} else {
|
|
332
|
+
const el = document.createElement('div');
|
|
333
|
+
el.className = 'alert alert-error';
|
|
334
|
+
el.id = 'login-error';
|
|
335
|
+
el.innerHTML = `<svg viewBox="0 0 24 24" width="15" height="15"><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"/></svg> ${msg}`;
|
|
336
|
+
this.insertBefore(el, this.firstChild);
|
|
337
|
+
}
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const btn = document.getElementById('login-btn');
|
|
342
|
+
const icon = document.getElementById('login-icon');
|
|
343
|
+
btn.disabled = true;
|
|
344
|
+
icon.innerHTML = `<circle cx="12" cy="12" r="10" stroke-dasharray="32" stroke-dashoffset="32" class="spin"/>`;
|
|
345
|
+
btn.lastChild.textContent = ' Signing in…';
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// Auto-focus password if email is pre-filled
|
|
349
|
+
const emailVal = document.getElementById('email').value;
|
|
350
|
+
if (emailVal) document.getElementById('password').focus();
|
|
351
|
+
</script>
|
|
352
|
+
|
|
353
|
+
</body>
|
|
354
|
+
</html>
|