rustio-admin 0.21.1

Django Admin, but for Rust. A small, focused admin framework.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
/* ============================================================
 * rustio-admin / pages / dashboard
 *
 * Operator-facing "Site administration" home. Five vertical
 * regions:
 *
 *   1. Greeting header  — friendly hello + page title + env badge.
 *   2. Stats strip      — four pastel tiles using semantic `-bg`
 *                         tokens (info / success / warning / accent).
 *   3. Model grid       — one flat CSS-grid of every project model.
 *                         Tiles cycle through four accent palettes
 *                         via `:nth-child` for visual rhythm; the
 *                         grid auto-fills to 3-4 columns at desktop
 *                         width and collapses cleanly on narrow.
 *   4. Activity feed +  — two-column split below the grid. Recent
 *      Framework tools     audit rows on the left (~2/3); a card
 *                          of framework-tool links on the right
 *                          (~1/3). Stacks on narrow viewports.
 *   5. (recent activity in empty-state form when no audit rows.)
 *
 * Doctrine: light + restrained. Pastel tints come from the
 * framework's already-calibrated semantic-color tokens; the
 * cycle adds visual variety without departing from the palette.
 * ============================================================ */

/* v0.19 — `.rio-dashboard-greeting` and its descendant selectors
 * (`__text`, `__title`, `__deck`, `__badges`, `__env*`, `__ver`)
 * are retired. The dashboard now uses the unified
 * `.rio-page-header` primitive (cards.css). Status chips were
 * extracted into `.rio-env-chip` below so other pages can drop
 * them next to their page titles too. */

/* Env / version chip — small, quiet metadata next to a page
 * title. Used by the dashboard's header and (in later turns) any
 * page that wants to surface the environment or framework
 * version inline. */
.rio-page-header__chips {
  display: inline-flex;
  align-items: center;
  gap: var(--rio-s2);
  flex-shrink: 0;
}
.rio-env-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  font-size: var(--rio-fs-xs);
  font-weight: var(--rio-fw-semibold);
  text-transform: uppercase;
  letter-spacing: var(--rio-tracking-allcaps);
  border-radius: 999px;
  background: var(--rio-surface-2);
  color: var(--rio-text-muted);
  border: 1px solid var(--rio-border-soft);
}
.rio-env-chip::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
}
.rio-env-chip--prod {
  background: var(--rio-success-bg);
  color: var(--rio-success);
  border-color: transparent;
}
.rio-env-chip--dev {
  background: var(--rio-warning-bg);
  color: var(--rio-warning);
  border-color: transparent;
}
.rio-env-chip--ver {
  font-family: var(--rio-font-mono);
  text-transform: none;
  letter-spacing: 0;
  color: var(--rio-text-subtle);
}
.rio-env-chip--ver::before { display: none; }

/* ---- Stats strip ---------------------------------------------- */

/* v0.19 — Unified stat tile. Pre-0.19 cycled four pastel
 * background fills (`--rio-info-bg` / `--rio-success-bg` /
 * `--rio-warning-bg` / `--rio-accent`-tint) for visual variety,
 * but the result read as "festive" rather than "professional".
 * The new tile is one consistent white surface with a 3-px
 * top accent bar — Stripe Dashboard / Linear / Vercel
 * convention. Color is reserved for SEMANTIC meaning
 * (success/warning) rather than per-tile decoration.
 *
 * Variants (`.rio-stat--info` etc.) are preserved as kept
 * selectors so existing markup keeps working; they only flip
 * the top accent bar's color now, not the whole tile fill.
 */
.rio-dashboard-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--rio-s3);
  margin-bottom: var(--rio-s5);
}
.rio-stat {
  position: relative;
  padding: var(--rio-s5);
  border-radius: var(--rio-radius);
  border: 1px solid var(--rio-border-soft);
  background: var(--rio-surface);
  box-shadow: var(--rio-shadow-xs);
  overflow: hidden;
  transition: border-color 0.12s, box-shadow 0.12s;
}
.rio-stat::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  inset-inline-end: 0;
  top: 0;
  height: 3px;
  background: var(--rio-border);
  /* Default rail: a quiet neutral line so all tiles share the same
   * top edge silhouette. Variants below repaint it with semantic
   * colors — used as accent dots, not whole-tile floods. */
}
.rio-stat--info::before    { background: var(--rio-accent); }
.rio-stat--success::before { background: var(--rio-success); }
.rio-stat--warning::before { background: var(--rio-warning); }
.rio-stat--accent::before  { background: var(--rio-accent); }
.rio-stat--danger::before  { background: var(--rio-danger); }

.rio-stat__label {
  margin: 0 0 var(--rio-s3);
  font-size: var(--rio-fs-xs);
  font-weight: var(--rio-fw-semibold);
  text-transform: uppercase;
  letter-spacing: var(--rio-tracking-allcaps);
  color: var(--rio-text-muted);
}
.rio-stat__value {
  margin: 0 0 var(--rio-s2);
  /* v0.19 — 2.25rem (~36 px) up from 2rem so the number reads
   * as the primary data point of the tile. */
  font-size: 2.25rem;
  font-weight: var(--rio-fw-bold);
  line-height: 1.05;
  color: var(--rio-text-strong);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.015em;
}
.rio-stat__value--text {
  font-size: 1.125rem;
  letter-spacing: var(--rio-tracking-heading);
  font-weight: var(--rio-fw-semibold);
}
.rio-stat__meta {
  margin: 0;
  font-size: var(--rio-fs-xs);
  color: var(--rio-text-subtle);
}

/* v0.19 — `.rio-dashboard-section-{head,label,count,link}`
 * selectors are retired. The dashboard's section headers now use
 * the shared `.rio-section` primitive in cards.css. Section-level
 * link (e.g. "View full history →") moves to the new
 * `.rio-section__link` selector below. */
.rio-section__link {
  font-size: var(--rio-fs-sm);
  color: var(--rio-accent);
  text-decoration: none;
  font-weight: var(--rio-fw-medium);
  white-space: nowrap;
}
.rio-section__link:hover { text-decoration: underline; }

/* ---- Model tile grid ------------------------------------------ */
/* v0.19 — `.rio-dashboard-models` wrapper retired; the dashboard
 * now wraps the grid in the shared `.rio-section`. */
.rio-model-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
  gap: var(--rio-s3);
}

.rio-model-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--rio-s2);
  padding: var(--rio-s4) var(--rio-s5);
  background: var(--rio-surface);
  border: 1px solid var(--rio-border-soft);
  border-radius: var(--rio-radius);
  transition: transform 0.14s, box-shadow 0.14s, border-color 0.14s;
  overflow: hidden;
}
.rio-model-tile::before {
  /* Top accent rail. Colour cycles every 4 tiles via `:nth-child`
   * below; the default value here is the framework accent, used by
   * tiles 4n+1. */
  content: "";
  position: absolute;
  top: 0;
  inset-inline-start: 0;
  inset-inline-end: 0;
  height: 3px;
  background: var(--rio-accent);
}
.rio-model-tile:hover {
  transform: translateY(-2px);
  border-color: var(--rio-border);
  box-shadow: var(--rio-shadow);
}

/* Four-tile colour cycle for visual rhythm. Tile body is muted
 * almost to plain surface — `color-mix(... 4% in srgb)` over
 * `--rio-surface` keeps just enough hue to differentiate without
 * the page reading as "colourful". The 3 px top rail carries the
 * real signal at full saturation; the body tint is the lightest
 * possible companion. */
.rio-model-tile:nth-child(4n+1) {
  background: color-mix(in srgb, var(--rio-accent) 2%, var(--rio-surface));
}
.rio-model-tile:nth-child(4n+1)::before { background: var(--rio-accent); }

.rio-model-tile:nth-child(4n+2) {
  background: color-mix(in srgb, var(--rio-success) 2%, var(--rio-surface));
}
.rio-model-tile:nth-child(4n+2)::before { background: var(--rio-success); }

.rio-model-tile:nth-child(4n+3) {
  background: color-mix(in srgb, var(--rio-warning) 2%, var(--rio-surface));
}
.rio-model-tile:nth-child(4n+3)::before { background: var(--rio-warning); }

.rio-model-tile:nth-child(4n+4) {
  background: color-mix(in srgb, var(--rio-danger) 2%, var(--rio-surface));
}
.rio-model-tile:nth-child(4n+4)::before { background: var(--rio-danger); }

.rio-model-tile__app {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  font-size: var(--rio-fs-xs);
  font-weight: var(--rio-fw-semibold);
  text-transform: uppercase;
  letter-spacing: var(--rio-tracking-allcaps);
  color: var(--rio-text-muted);
  background: rgb(255 255 255 / 0.6);
  border: 1px solid var(--rio-border-soft);
  border-radius: 999px;
  align-self: flex-start;
}

.rio-model-tile__title-link { text-decoration: none; color: inherit; }
.rio-model-tile__title {
  margin: 0;
  font-size: var(--rio-fs-md);
  font-weight: var(--rio-fw-semibold);
  color: var(--rio-text-strong);
  letter-spacing: var(--rio-tracking-heading);
}
.rio-model-tile__title-link:hover .rio-model-tile__title { color: var(--rio-accent); }

.rio-model-tile__stat {
  display: flex;
  align-items: baseline;
  gap: var(--rio-s2);
  margin: var(--rio-s1) 0 0;
}
.rio-model-tile__stat-num {
  font-size: 1.85rem;
  font-weight: var(--rio-fw-bold);
  line-height: 1;
  color: var(--rio-text-strong);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}
.rio-model-tile__stat-suffix {
  font-size: var(--rio-fs-sm);
  font-weight: var(--rio-fw-regular);
  color: var(--rio-text-muted);
}
.rio-model-tile__stat-secondary {
  margin: var(--rio-s1) 0 0;
  font-size: var(--rio-fs-sm);
  color: var(--rio-text-muted);
}
.rio-model-tile__stat-secondary .rio-model-tile__stat-num {
  font-size: var(--rio-fs-base);
  font-weight: var(--rio-fw-semibold);
  color: var(--rio-text);
  margin-inline-end: var(--rio-s1);
}
.rio-model-tile__sparkline {
  display: block;
  width: 100%;
  height: 20px;
  margin: var(--rio-s2) 0 var(--rio-s1);
  overflow: visible;
}
.rio-model-tile__sparkline-bar {
  fill: var(--rio-accent);
  opacity: 0.7;
}
.rio-model-tile__meta {
  margin: 0;
  font-size: var(--rio-fs-xs);
  color: var(--rio-text-subtle);
}
.rio-model-tile__actions {
  display: flex;
  gap: var(--rio-s2);
  margin-top: auto;
  padding-top: var(--rio-s3);
  border-top: 1px solid rgb(0 0 0 / 0.06);
}

/* ---- Activity feed + Framework tools (two-column split) ------- */

.rio-dashboard-split {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: var(--rio-s5);
}
@media (max-width: 959px) {
  .rio-dashboard-split { grid-template-columns: 1fr; }
}

.rio-dashboard-activity,
.rio-dashboard-tools { min-width: 0; }

.rio-sparkline {
  margin: 0 0 var(--rio-s4);
  padding: var(--rio-s4) var(--rio-s5);
  border: 1px solid var(--rio-border-soft);
  border-radius: var(--rio-radius);
  background: var(--rio-surface);
}
.rio-sparkline__caption {
  font-size: var(--rio-fs-sm);
  color: var(--rio-text-muted);
  margin: 0 0 var(--rio-s3);
}
.rio-sparkline__svg {
  display: block;
  width: 100%;
  height: 64px;
  overflow: visible;
}
/* v0.18.6 — bar chart replaced by an area chart. .rio-sparkline__bar
 * and .rio-sparkline__track from 0.18.5 are intentionally left in
 * the cascade unchanged so any third-party templates that still
 * render them keep working; the framework's index.html no longer
 * emits either element. */
.rio-sparkline__bar { fill: var(--rio-accent); opacity: 0.85; }
.rio-sparkline__track { fill: var(--rio-border); opacity: 0.55; }

/* Area-chart elements — baseline rule + gradient-filled polygon
 * under the data line, the line itself, and a dot at every
 * measurement. Matches the analytics-tool convention (Vercel /
 * Linear / Stripe Dashboard / Grafana). */
.rio-sparkline__baseline {
  stroke: var(--rio-border);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}
.rio-sparkline__area {
  fill: url(#rio-sparkline-grad);
  /* `preserveAspectRatio="none"` on the parent <svg> stretches
   * the polygon horizontally; that's intended (it's a sparkline
   * sized by its container) but it means the *vertical* gradient
   * still resolves correctly. */
}
.rio-sparkline__grad-stop-top { stop-color: var(--rio-accent); stop-opacity: 0.28; }
.rio-sparkline__grad-stop-bot { stop-color: var(--rio-accent); stop-opacity: 0; }
.rio-sparkline__line {
  fill: none;
  stroke: var(--rio-accent);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  /* Keep the stroke 2-px on screen regardless of the horizontal
   * stretch — without this, the line would visually thin out as
   * the chart widens beyond the 280-unit viewBox. */
  vector-effect: non-scaling-stroke;
}
.rio-sparkline__dot {
  fill: var(--rio-surface);
  stroke: var(--rio-accent);
  stroke-width: 1.5;
  vector-effect: non-scaling-stroke;
}
.rio-sparkline__tick {
  fill: var(--rio-text-muted);
  font-size: 10px;
  font-family: var(--rio-font-sans, system-ui);
}

.rio-activity-feed {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--rio-border-soft);
  border-radius: var(--rio-radius);
  background: var(--rio-surface);
  overflow: hidden;
}
.rio-activity-feed__item {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--rio-s3);
  padding: var(--rio-s3) var(--rio-s5);
  border-bottom: 1px solid var(--rio-border-soft);
  font-size: var(--rio-fs-sm);
  color: var(--rio-text);
}
.rio-activity-feed__item:last-child { border-bottom: 0; }
.rio-activity-feed__item:nth-child(even) { background: var(--rio-surface-2); }

.rio-activity-feed__target { font-weight: var(--rio-fw-medium); color: var(--rio-text-strong); }
.rio-activity-feed__target a { color: var(--rio-text-strong); text-decoration: none; }
.rio-activity-feed__target a:hover { color: var(--rio-accent); }
.rio-activity-feed__sep { color: var(--rio-text-subtle); margin: 0 var(--rio-s1); }
.rio-activity-feed__summary {
  flex: 1;
  min-width: 0;
  color: var(--rio-text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.rio-activity-feed__meta {
  margin-inline-start: auto;
  font-size: var(--rio-fs-xs);
  color: var(--rio-text-muted);
  font-variant-numeric: tabular-nums;
}

/* ---- Framework tools card ------------------------------------- */

.rio-tool-list {
  list-style: none;
  margin: 0;
  padding: 0;
  border: 1px solid var(--rio-border-soft);
  border-radius: var(--rio-radius);
  background: var(--rio-surface);
  overflow: hidden;
}
.rio-tool-link {
  display: flex;
  align-items: flex-start;
  gap: var(--rio-s3);
  padding: var(--rio-s3) var(--rio-s4);
  border-bottom: 1px solid var(--rio-border-soft);
  text-decoration: none;
  color: var(--rio-text);
  transition: background-color 0.12s;
}
.rio-tool-list li:last-child .rio-tool-link { border-bottom: 0; }
.rio-tool-link:hover { background: var(--rio-surface-2); }

.rio-tool-link__icon {
  flex-shrink: 0;
  width: 18px;
  height: 18px;
  color: var(--rio-accent);
  margin-top: 2px;
}
.rio-tool-link__text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.rio-tool-link__text strong {
  font-size: var(--rio-fs-sm);
  font-weight: var(--rio-fw-semibold);
  color: var(--rio-text-strong);
}
.rio-tool-link__text span {
  font-size: var(--rio-fs-xs);
  color: var(--rio-text-muted);
  line-height: 1.4;
}