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.
@@ -1,61 +1,66 @@
1
1
  {% extends "layouts/base.njk" %}
2
-
3
2
  {% block title %}Dashboard{% endblock %}
4
3
 
5
4
  {% block content %}
6
5
  <div class="breadcrumb">
7
- <span>🏠</span>
6
+ <span class="icon icon-13" style="color:var(--text-xmuted)"><svg viewBox="0 0 24 24"><use href="#ic-grid"/></svg></span>
8
7
  <span class="breadcrumb-sep">β€Ί</span>
9
- <span>Dashboard</span>
8
+ <span class="breadcrumb-current">Dashboard</span>
10
9
  </div>
11
10
 
12
11
  {# ── Stat cards ── #}
12
+ {% if resources | length %}
13
13
  <div class="stats-grid">
14
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>
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>
21
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>
22
24
  </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
25
  {% endfor %}
31
26
  </div>
27
+ {% endif %}
32
28
 
33
- {# ── Quick start if no resources ── #}
29
+ {# ── No resources welcome state ── #}
34
30
  {% if not resources | length %}
35
31
  <div class="card">
36
32
  <div class="card-body">
37
33
  <div class="empty-state">
38
- <div class="empty-icon">πŸš€</div>
34
+ <div class="empty-icon">
35
+ <span class="icon icon-24"><svg viewBox="0 0 24 24"><use href="#ic-database"/></svg></span>
36
+ </div>
39
37
  <div class="empty-title">Welcome to Millas Admin</div>
40
38
  <div class="empty-desc">
41
39
  Register your models to start managing your data.
42
- Add this to your <code>AppServiceProvider.boot()</code>:
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>:
43
41
  </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();
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();
46
43
  Admin.register(UserResource);
47
- Admin.mount(Route, expressApp);</pre>
44
+ Admin.mount(expressApp);</pre>
48
45
  </div>
49
46
  </div>
50
47
  </div>
48
+
51
49
  {% else %}
52
- {# ── Recent activity per resource ── #}
50
+
51
+ {# ── Recent records per resource ── #}
53
52
  {% for resource in resources %}
54
53
  {% if resource.recent | length %}
55
- <div class="card mb-6">
54
+ <div class="card mb-5">
56
55
  <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>
56
+ <span class="card-title">
57
+ <span class="icon icon-15" style="color:var(--primary)"><svg viewBox="0 0 24 24"><use href="#ic-table"/></svg></span>
58
+ Recent {{ resource.label }}
59
+ </span>
60
+ <a href="{{ adminPrefix }}/{{ resource.slug }}" class="btn btn-ghost btn-sm">
61
+ View all
62
+ <span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-chevron-right"/></svg></span>
63
+ </a>
59
64
  </div>
60
65
  <div class="table-wrap">
61
66
  <table>
@@ -80,5 +85,6 @@ Admin.mount(Route, expressApp);</pre>
80
85
  </div>
81
86
  {% endif %}
82
87
  {% endfor %}
88
+
83
89
  {% endif %}
84
90
  {% endblock %}
@@ -1,145 +1,262 @@
1
1
  {% extends "layouts/base.njk" %}
2
2
 
3
3
  {% block title %}{{ 'Edit' if isEdit else 'New' }} {{ resource.singular }}{% endblock %}
4
- {% block topbar_title %}{{ resource.icon }} {{ 'Edit' if isEdit else 'New' }} {{ resource.singular }}{% endblock %}
4
+ {% block topbar_title %}
5
+ <span class="icon icon-16" style="color:var(--text-muted)">
6
+ <svg viewBox="0 0 24 24"><use href="#ic-{{ 'edit' if isEdit else 'plus' }}"/></svg>
7
+ </span>
8
+ {{ 'Edit' if isEdit else 'New' }} {{ resource.singular }}
9
+ {% endblock %}
10
+
11
+ {% block topbar_actions %}
12
+ <a href="{{ adminPrefix }}/{{ resource.slug }}" class="btn btn-ghost btn-sm">
13
+ <span class="icon icon-13"><svg viewBox="0 0 24 24"><use href="#ic-arrow-left"/></svg></span>
14
+ Back to {{ resource.label }}
15
+ </a>
16
+ {% endblock %}
5
17
 
6
18
  {% block content %}
7
19
  <div class="breadcrumb">
8
- <a href="{{ adminPrefix }}/">Dashboard</a>
20
+ <a href="{{ adminPrefix }}/">
21
+ <span class="icon icon-12"><svg viewBox="0 0 24 24"><use href="#ic-grid"/></svg></span>
22
+ </a>
9
23
  <span class="breadcrumb-sep">β€Ί</span>
10
24
  <a href="{{ adminPrefix }}/{{ resource.slug }}">{{ resource.label }}</a>
11
25
  <span class="breadcrumb-sep">β€Ί</span>
12
- <span>{{ 'Edit #' + record.id if isEdit else 'New' }}</span>
26
+ <span class="breadcrumb-current">{{ 'Edit #' + record.id if isEdit else 'New ' + resource.singular }}</span>
13
27
  </div>
14
28
 
15
- <div class="card" style="max-width:720px">
16
- <div class="card-header">
17
- <span class="card-title">{{ 'Edit ' + resource.singular + ' #' + record.id if isEdit else 'Create ' + resource.singular }}</span>
18
- </div>
19
- <div class="card-body">
20
- <form method="POST" action="{{ formAction }}">
29
+ <div style="max-width:760px">
30
+ <div class="card">
31
+ <div class="card-header">
32
+ <span class="card-title">
33
+ <span class="icon icon-15" style="color:var(--primary)">
34
+ <svg viewBox="0 0 24 24"><use href="#ic-{{ 'edit' if isEdit else 'file' }}"/></svg>
35
+ </span>
36
+ {{ ('Edit ' + resource.singular + ' #' + record.id) if isEdit else ('Create ' + resource.singular) }}
37
+ </span>
21
38
  {% if isEdit %}
22
- <input type="hidden" name="_method" value="PUT">
39
+ <span class="badge badge-gray text-xs">ID: {{ record.id }}</span>
23
40
  {% endif %}
41
+ </div>
24
42
 
43
+ <div class="card-body">
44
+
45
+ {# ── Validation errors summary ── #}
25
46
  {% if errors | length %}
26
- <div class="alert alert-error" style="margin-bottom:20px">
47
+ <div class="alert alert-error mb-5">
48
+ <span class="icon icon-16"><svg viewBox="0 0 24 24"><use href="#ic-alert-circle"/></svg></span>
27
49
  <div>
28
50
  <strong>Please fix the following errors:</strong>
29
- <ul style="margin-top:6px;padding-left:16px">
51
+ <ul style="margin-top:5px;padding-left:14px;list-style:disc">
30
52
  {% for field, msgs in errors %}
31
- <li>{{ msgs[0] }}</li>
53
+ <li style="font-size:12.5px;margin-top:2px">{{ msgs[0] if msgs is iterable and msgs[0] is string else msgs }}</li>
32
54
  {% endfor %}
33
55
  </ul>
34
56
  </div>
35
57
  </div>
36
58
  {% endif %}
37
59
 
38
- <div class="form-grid">
39
- {% for field in formFields %}
40
- <div class="form-group {% if field.type == 'textarea' or field.type == 'json' %}full{% endif %}">
41
- <label class="form-label">
42
- {{ field.label }}
43
- {% if field.required %}<span class="required">*</span>{% endif %}
44
- </label>
45
-
46
- {% set val = record[field.name] if record[field.name] is defined else '' %}
47
- {% set hasError = errors[field.name] is defined %}
48
-
49
- {% if field.type == 'select' and field.options %}
50
- <select name="{{ field.name }}" class="form-control {% if hasError %}error{% endif %}">
51
- <option value="">β€” Select β€”</option>
52
- {% for opt in field.options %}
53
- <option value="{{ opt }}" {% if val == opt %}selected{% endif %}>{{ opt }}</option>
54
- {% endfor %}
55
- </select>
56
-
57
- {% elif field.type == 'boolean' %}
58
- <select name="{{ field.name }}" class="form-control">
59
- <option value="1" {% if val %}selected{% endif %}>Yes</option>
60
- <option value="0" {% if not val %}selected{% endif %}>No</option>
61
- </select>
62
-
63
- {% elif field.type == 'textarea' %}
64
- <textarea
65
- name="{{ field.name }}"
66
- class="form-control {% if hasError %}error{% endif %}"
67
- placeholder="{{ field.placeholder or '' }}"
68
- rows="4">{{ val }}</textarea>
69
-
70
- {% elif field.type == 'json' %}
71
- <textarea
72
- name="{{ field.name }}"
73
- class="form-control {% if hasError %}error{% endif %}"
74
- placeholder='{"key": "value"}'
75
- style="font-family:monospace;font-size:12px"
76
- rows="5">{{ val | dump if val else '' }}</textarea>
77
-
78
- {% elif field.type == 'password' %}
79
- <input
80
- type="password"
81
- name="{{ field.name }}"
82
- class="form-control {% if hasError %}error{% endif %}"
83
- placeholder="{{ 'Leave blank to keep current' if isEdit else '' }}">
84
-
85
- {% elif field.type == 'email' %}
86
- <input
87
- type="email"
88
- name="{{ field.name }}"
89
- value="{{ val }}"
90
- class="form-control {% if hasError %}error{% endif %}"
91
- placeholder="{{ field.placeholder or '' }}">
92
-
93
- {% elif field.type == 'number' %}
94
- <input
95
- type="number"
96
- name="{{ field.name }}"
97
- value="{{ val }}"
98
- class="form-control {% if hasError %}error{% endif %}"
99
- placeholder="{{ field.placeholder or '0' }}">
100
-
101
- {% elif field.type == 'date' %}
102
- <input
103
- type="date"
104
- name="{{ field.name }}"
105
- value="{{ val }}"
106
- class="form-control {% if hasError %}error{% endif %}">
107
-
108
- {% else %}
109
- <input
110
- type="text"
111
- name="{{ field.name }}"
112
- value="{{ val }}"
113
- class="form-control {% if hasError %}error{% endif %}"
114
- placeholder="{{ field.placeholder or '' }}">
115
- {% endif %}
60
+ <form method="POST" action="{{ formAction }}" id="record-form" novalidate>
61
+ {% if isEdit %}
62
+ <input type="hidden" name="_method" value="PUT">
63
+ {% endif %}
64
+
65
+ <div class="form-grid">
66
+ {% for field in formFields %}
67
+ <div class="form-group {% if field.type == 'textarea' or field.type == 'json' %}full{% endif %}">
68
+ <label class="form-label" for="field-{{ field.name }}">
69
+ {{ field.label }}
70
+ {% if field.required %}<span class="required">*</span>{% endif %}
71
+ </label>
72
+
73
+ {% set val = record[field.name] if record[field.name] is defined else '' %}
74
+ {% set hasError = errors[field.name] is defined %}
75
+
76
+ {# ── Select ── #}
77
+ {% if field.type == 'select' and field.options %}
78
+ <select
79
+ id="field-{{ field.name }}"
80
+ name="{{ field.name }}"
81
+ class="form-control{% if hasError %} error{% endif %}">
82
+ <option value="">β€” Select β€”</option>
83
+ {% for opt in field.options %}
84
+ <option value="{{ opt }}" {% if val == opt %}selected{% endif %}>{{ opt }}</option>
85
+ {% endfor %}
86
+ </select>
87
+
88
+ {# ── Boolean ── #}
89
+ {% elif field.type == 'boolean' %}
90
+ <div class="check-group" style="padding:8px 0">
91
+ <input
92
+ type="checkbox"
93
+ id="field-{{ field.name }}"
94
+ name="{{ field.name }}"
95
+ class="check-input"
96
+ value="1"
97
+ {% if val %}checked{% endif %}>
98
+ <label class="check-label" for="field-{{ field.name }}">
99
+ {{ field.label }}
100
+ </label>
101
+ </div>
102
+
103
+ {# ── Textarea ── #}
104
+ {% elif field.type == 'textarea' %}
105
+ <textarea
106
+ id="field-{{ field.name }}"
107
+ name="{{ field.name }}"
108
+ class="form-control{% if hasError %} error{% endif %}"
109
+ placeholder="{{ field.placeholder or '' }}"
110
+ rows="4">{{ val }}</textarea>
111
+
112
+ {# ── JSON ── #}
113
+ {% elif field.type == 'json' %}
114
+ <textarea
115
+ id="field-{{ field.name }}"
116
+ name="{{ field.name }}"
117
+ class="form-control{% if hasError %} error{% endif %}"
118
+ placeholder='{"key": "value"}'
119
+ style="font-family:'DM Mono',monospace;font-size:12px"
120
+ rows="5">{{ val | dump if val else '' }}</textarea>
121
+
122
+ {# ── Password ── #}
123
+ {% elif field.type == 'password' %}
124
+ <div style="position:relative">
125
+ <input
126
+ type="password"
127
+ id="field-{{ field.name }}"
128
+ name="{{ field.name }}"
129
+ class="form-control{% if hasError %} error{% endif %}"
130
+ placeholder="{{ 'Leave blank to keep current' if isEdit else '' }}"
131
+ style="padding-right:40px">
132
+ <button type="button"
133
+ onclick="togglePassword(this)"
134
+ style="position:absolute;right:10px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;color:var(--text-muted);display:flex;align-items:center">
135
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
136
+ <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"/>
137
+ </svg>
138
+ </button>
139
+ </div>
116
140
 
117
- {% if hasError %}
118
- <span class="form-error">{{ errors[field.name][0] }}</span>
119
- {% elif field.help %}
120
- <span class="form-help">{{ field.help }}</span>
141
+ {# ── Email ── #}
142
+ {% elif field.type == 'email' %}
143
+ <input
144
+ type="email"
145
+ id="field-{{ field.name }}"
146
+ name="{{ field.name }}"
147
+ value="{{ val }}"
148
+ class="form-control{% if hasError %} error{% endif %}"
149
+ placeholder="{{ field.placeholder or 'email@example.com' }}">
150
+
151
+ {# ── Number ── #}
152
+ {% elif field.type == 'number' %}
153
+ <input
154
+ type="number"
155
+ id="field-{{ field.name }}"
156
+ name="{{ field.name }}"
157
+ value="{{ val }}"
158
+ class="form-control{% if hasError %} error{% endif %}"
159
+ placeholder="{{ field.placeholder or '0' }}">
160
+
161
+ {# ── Date ── #}
162
+ {% elif field.type == 'date' %}
163
+ <input
164
+ type="date"
165
+ id="field-{{ field.name }}"
166
+ name="{{ field.name }}"
167
+ value="{{ val }}"
168
+ class="form-control{% if hasError %} error{% endif %}">
169
+
170
+ {# ── Datetime ── #}
171
+ {% elif field.type == 'datetime' %}
172
+ <input
173
+ type="datetime-local"
174
+ id="field-{{ field.name }}"
175
+ name="{{ field.name }}"
176
+ value="{{ val }}"
177
+ class="form-control{% if hasError %} error{% endif %}">
178
+
179
+ {# ── Default text ── #}
180
+ {% else %}
181
+ <input
182
+ type="text"
183
+ id="field-{{ field.name }}"
184
+ name="{{ field.name }}"
185
+ value="{{ val }}"
186
+ class="form-control{% if hasError %} error{% endif %}"
187
+ placeholder="{{ field.placeholder or '' }}">
188
+ {% endif %}
189
+
190
+ {# ── Error / help ── #}
191
+ {% if hasError %}
192
+ <span class="form-error">
193
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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>
194
+ {{ errors[field.name][0] if errors[field.name] is iterable and errors[field.name][0] is string else errors[field.name] }}
195
+ </span>
196
+ {% elif field.help %}
197
+ <span class="form-help">{{ field.help }}</span>
198
+ {% endif %}
199
+ </div>
200
+ {% endfor %}
201
+ </div>
202
+
203
+ {# ── Form footer ── #}
204
+ <div class="flex items-center gap-2" style="margin-top:24px;padding-top:20px;border-top:1px solid var(--border-soft)">
205
+ <button type="submit" class="btn btn-primary" id="submit-btn">
206
+ <span class="icon icon-14"><svg viewBox="0 0 24 24"><use href="#ic-save"/></svg></span>
207
+ {{ 'Save Changes' if isEdit else 'Create ' + resource.singular }}
208
+ </button>
209
+ <a href="{{ adminPrefix }}/{{ resource.slug }}" class="btn btn-ghost">Cancel</a>
210
+
211
+ {% if isEdit and resource.canDelete %}
212
+ <button type="button" class="btn btn-danger" style="margin-left:auto"
213
+ onclick="confirmDelete('{{ adminPrefix }}/{{ resource.slug }}/{{ record.id }}/delete', '{{ resource.singular }} #{{ record.id }}')">
214
+ <span class="icon icon-14"><svg viewBox="0 0 24 24"><use href="#ic-trash"/></svg></span>
215
+ Delete
216
+ </button>
121
217
  {% endif %}
122
218
  </div>
123
- {% endfor %}
124
- </div>
219
+ </form>
220
+ </div>
221
+ </div>
125
222
 
126
- <div class="flex items-center gap-2" style="margin-top:24px;padding-top:20px;border-top:1px solid var(--border)">
127
- <button type="submit" class="btn btn-primary">
128
- {{ 'πŸ’Ύ Save Changes' if isEdit else 'οΌ‹ Create ' + resource.singular }}
129
- </button>
130
- <a href="{{ adminPrefix }}/{{ resource.slug }}" class="btn btn-ghost">Cancel</a>
131
- {% if isEdit and resource.canDelete %}
132
- <button type="button" class="btn btn-danger" style="margin-left:auto"
133
- onclick="confirmDelete('{{ adminPrefix }}/{{ resource.slug }}/{{ record.id }}/delete', '{{ resource.singular }} #{{ record.id }}')">
134
- Delete
135
- </button>
136
- {% endif %}
137
- </div>
138
- </form>
223
+ {# ── Timestamps readonly ── #}
224
+ {% if isEdit and (record.created_at or record.updated_at) %}
225
+ <div class="flex gap-4" style="margin-top:14px;padding:0 2px">
226
+ {% if record.created_at %}
227
+ <span class="text-xs text-muted flex items-center gap-1">
228
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
229
+ Created {{ record.created_at }}
230
+ </span>
231
+ {% endif %}
232
+ {% if record.updated_at %}
233
+ <span class="text-xs text-muted flex items-center gap-1">
234
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><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"/></svg>
235
+ Updated {{ record.updated_at }}
236
+ </span>
237
+ {% endif %}
139
238
  </div>
239
+ {% endif %}
140
240
  </div>
141
241
 
242
+ <script>
243
+ function togglePassword(btn) {
244
+ const input = btn.previousElementSibling || btn.closest('div').querySelector('input');
245
+ if (!input) return;
246
+ input.type = input.type === 'password' ? 'text' : 'password';
247
+ }
248
+
249
+ // Prevent double-submit
250
+ document.getElementById('record-form')?.addEventListener('submit', function() {
251
+ const btn = document.getElementById('submit-btn');
252
+ if (btn) {
253
+ btn.disabled = true;
254
+ btn.innerHTML = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="animation:spin 1s linear infinite"><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"/></svg> Saving…`;
255
+ }
256
+ });
257
+ </script>
258
+
142
259
  <style>
143
- .form-control.error { border-color: var(--danger); }
260
+ @keyframes spin { to { transform: rotate(360deg); } }
144
261
  </style>
145
262
  {% endblock %}