rustio-admin 0.20.0

Django Admin, but for Rust. A small, focused admin framework.
Documentation
/* ============================================================
 * rustio-admin / components / cards
 *
 * Surface containers — page headers, breadcrumbs, action rows,
 * the canonical card box, the empty-state placeholder, and the
 * .rio-meta low-emphasis caption. These are the page-chrome
 * primitives every screen leans on.
 * ============================================================ */

/* v0.19 — "Quiet expert" surface language. Pre-0.19 cards leaned
 * on `--rio-shadow` (4-px blur) which over-stated their lift on the
 * pale canvas. New baseline:
 *   - Cards use the lighter `--rio-shadow-xs` (1-px lift), still
 *     paired with `border-soft` for the edge — what Stripe/Linear
 *     call a "raised surface" without theatrical depth.
 *   - `.rio-card--quiet` opts out of the shadow entirely, for
 *     inline cards stacked inside other regions.
 *   - `.rio-card--elevated` opts INTO the heavier `--rio-shadow`
 *     for floating panels (modals, dropdowns already use
 *     `--rio-shadow-lg`; this is the in-between tier).
 *
 * Page header tightened: smaller breadcrumb, leaner spacing.
 * Section primitive added (`.rio-section`) for the rhythm
 * between major page regions without dropping a heavy card each
 * time.
 *
 * Empty state promoted from a one-paragraph note to a proper
 * three-line block — icon slot + headline + meta + optional CTA. */

.rio-page-header {
  /* v0.19 — 28-px below header before the first section. Tightens
   * the dashboard's "headline cluster" so titles, deck, badges,
   * and the first KPI strip don't push the table below the fold
   * on a 1280-px laptop. */
  margin-bottom: var(--rio-s5);
}
.rio-breadcrumbs {
  font-size: var(--rio-fs-xs);
  color: var(--rio-text-subtle);
  font-weight: var(--rio-fw-medium);
  letter-spacing: 0.01em;
  margin-bottom: var(--rio-s2);
}
.rio-breadcrumbs a {
  color: var(--rio-text-muted);
  font-weight: var(--rio-fw-semibold);
}
.rio-breadcrumbs a:hover { color: var(--rio-text-strong); }
.rio-page-actions {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--rio-s4);
  flex-wrap: wrap;
}
.rio-page-actions h1 {
  color: var(--rio-text-strong);
  /* v0.19 — slightly tighter page title size for desktop chrome.
   * Was 1.85rem (~30px); 1.5rem (24px) matches the Stripe
   * Dashboard convention and leaves room for inline meta. */
  font-size: 1.5rem;
  line-height: 1.25;
  margin: 0;
  font-weight: var(--rio-fw-semibold);
  letter-spacing: -0.005em;
}
/* Optional meta line under the title (subtle, used as a deck). */
.rio-page-header__lead {
  margin: var(--rio-s2) 0 0;
  font-size: var(--rio-fs-md);
  color: var(--rio-text-muted);
}
/* Optional button cluster on the trailing edge of the page actions
 * row — used by pages with multiple secondary affordances (apis
 * index, csv import result). One row wraps onto multiple at narrow
 * widths; primary CTA sits last so it lands closest to the cursor's
 * natural arrival point. */
.rio-page-actions__group {
  display: inline-flex;
  align-items: center;
  gap: var(--rio-s2);
  flex-wrap: wrap;
}

.rio-card {
  background: var(--rio-surface);
  border: 1px solid var(--rio-border-soft);
  border-radius: var(--rio-radius-lg);
  padding: var(--rio-s6);
  margin-bottom: var(--rio-s5);
  box-shadow: var(--rio-shadow-xs);
}
.rio-card--quiet  { box-shadow: none; }
.rio-card--elevated { box-shadow: var(--rio-shadow); }
.rio-card-section {
  margin-top: var(--rio-s5);
  padding-top: var(--rio-s4);
  border-top: 1px solid var(--rio-border-soft);
}

/* v0.19 — Section primitive for page-level content rhythm.
 * Each major region on a page (KPIs, model grid, activity feed,
 * settings group) wraps in `.rio-section` so vertical rhythm
 * stays consistent without dropping a heavy card each time.
 * `.rio-section__label` is the muted eyebrow (uppercase),
 * `.rio-section__title` is the H2-sized region heading,
 * `.rio-section__lead` is the optional caption beneath. */
.rio-section {
  margin-bottom: var(--rio-s6);
}
.rio-section__header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--rio-s4);
  flex-wrap: wrap;
  margin-bottom: var(--rio-s3);
}
.rio-section__heading {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.rio-section__label {
  font-size: var(--rio-fs-xs);
  font-weight: var(--rio-fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--rio-text-subtle);
}
.rio-section__title {
  font-size: 1.125rem;       /* 18 px */
  font-weight: var(--rio-fw-semibold);
  color: var(--rio-text-strong);
  margin: 0;
  letter-spacing: -0.005em;
}
.rio-section__lead {
  font-size: var(--rio-fs-sm);
  color: var(--rio-text-muted);
  margin: 2px 0 0;
}
.rio-section__meta {
  font-size: var(--rio-fs-sm);
  color: var(--rio-text-muted);
}

/* v0.19 — Empty state primitive. Replaces the pre-0.19 one-line
 * `.rio-empty` paragraph. Same selector hierarchy as section so
 * it slots into a card or a section with no margin fuss.
 *   <div class="rio-empty-state">
 *     <div class="rio-empty-state__icon">{{ icon("plus") }}</div>
 *     <h3 class="rio-empty-state__title">No posts yet</h3>
 *     <p class="rio-empty-state__lead">Posts you create will
 *        show up here for everyone with access.</p>
 *     <a class="rio-button rio-button--primary" href="...">Add post</a>
 *   </div>                                                          */
.rio-empty {
  padding: var(--rio-s6);
  text-align: center;
  color: var(--rio-text-muted);
  font-size: var(--rio-fs-md);
}
.rio-empty-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--rio-s3);
  text-align: center;
  padding: var(--rio-s7) var(--rio-s5);
  color: var(--rio-text-muted);
}
.rio-empty-state__icon {
  width: 44px; height: 44px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 999px;
  background: var(--rio-surface-2);
  color: var(--rio-text-muted);
}
.rio-empty-state__icon .rio-icon { width: 22px; height: 22px; }
.rio-empty-state__title {
  margin: 0;
  font-size: 1rem;            /* 16 px */
  font-weight: var(--rio-fw-semibold);
  color: var(--rio-text-strong);
}
.rio-empty-state__lead {
  margin: 0;
  max-width: 36ch;
  font-size: var(--rio-fs-sm);
  line-height: var(--rio-lh-body);
}

.rio-meta {
  color: var(--rio-text-muted);
  font-size: var(--rio-fs-sm);
}

/* v0.19 — Confirm block primitive. One shape for every
 * destructive / mutating-action confirmation in the framework
 * (`confirm_delete`, `bulk_confirm_delete`, `bulk_confirm_action`,
 * `confirm_admin_action`, `user_confirm_delete`, `group_confirm_delete`,
 * `lock_user`). Pre-0.19 each template used `.rio-card.rio-confirm`
 * but no CSS rules were defined for it — confirmation pages looked
 * like ordinary cards with a plain paragraph + button row, missing
 * the "you are about to do something serious" weight.
 *
 * Structure:
 *   <section class="rio-confirm">
 *     <header class="rio-confirm__header">
 *       <div class="rio-confirm__icon">{{ icon(...) }}</div>
 *       <div>
 *         <h2 class="rio-confirm__title">Headline</h2>
 *         <p class="rio-confirm__lead">Description.</p>
 *       </div>
 *     </header>
 *     <!-- optional warnings -->
 *     <div class="rio-confirm__warnings">...</div>
 *     <!-- optional affected items / cascade list -->
 *     <ul class="rio-confirm__items">...</ul>
 *     <p class="rio-confirm__small">Final caveat / undo notice.</p>
 *     <form class="rio-form-actions">...</form>
 *   </section>
 *
 * Variants:
 *   .rio-confirm--danger   destructive (delete, lock, revoke)
 *   .rio-confirm--neutral  reversible bulk actions
 */
.rio-confirm {
  background: var(--rio-surface);
  border: 1px solid var(--rio-border-soft);
  border-radius: var(--rio-radius-lg);
  padding: var(--rio-s6);
  margin-bottom: var(--rio-s5);
  box-shadow: var(--rio-shadow-xs);
  /* Left-edge stripe carries the destructive cue without flooding
   * the card with red — calm-but-noticeable. */
  border-inline-start: 4px solid var(--rio-border);
}
.rio-confirm--danger {
  border-inline-start-color: var(--rio-danger);
}
.rio-confirm--neutral {
  border-inline-start-color: var(--rio-accent);
}
.rio-confirm__header {
  display: flex;
  align-items: flex-start;
  gap: var(--rio-s4);
  margin-bottom: var(--rio-s4);
}
.rio-confirm__icon {
  flex-shrink: 0;
  width: 44px; height: 44px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 999px;
  background: var(--rio-surface-2);
  color: var(--rio-text-muted);
}
.rio-confirm--danger .rio-confirm__icon {
  background: var(--rio-danger-bg);
  color: var(--rio-danger);
}
.rio-confirm--neutral .rio-confirm__icon {
  background: var(--rio-accent-soft);
  color: var(--rio-accent);
}
.rio-confirm__icon .rio-icon { width: 22px; height: 22px; }
.rio-confirm__title {
  margin: 0;
  font-size: 1.125rem;        /* 18 px */
  font-weight: var(--rio-fw-semibold);
  color: var(--rio-text-strong);
  letter-spacing: -0.005em;
}
.rio-confirm__lead {
  margin: var(--rio-s1) 0 0;
  font-size: var(--rio-fs-md);
  color: var(--rio-text);
  line-height: var(--rio-lh-body);
}
.rio-confirm__lead strong {
  color: var(--rio-text-strong);
  font-weight: var(--rio-fw-semibold);
}
.rio-confirm__lead code {
  font-family: var(--rio-font-mono);
  font-size: 0.92em;
  background: var(--rio-surface-2);
  padding: 1px 6px;
  border-radius: var(--rio-radius-sm);
}
.rio-confirm__warnings {
  display: flex;
  flex-direction: column;
  gap: var(--rio-s2);
  margin-bottom: var(--rio-s4);
}
.rio-confirm__warnings .rio-flash {
  margin: 0;
}
.rio-confirm__items {
  list-style: none;
  margin: 0 0 var(--rio-s4);
  padding: var(--rio-s3) 0;
  border-top: 1px solid var(--rio-border-soft);
  border-bottom: 1px solid var(--rio-border-soft);
}
.rio-confirm__items > li {
  padding: 2px 0;
  font-size: var(--rio-fs-sm);
  color: var(--rio-text);
}
.rio-confirm__items > li + li { border-top: 1px dashed var(--rio-border-soft); padding-top: 6px; margin-top: 6px; }
.rio-confirm__items code {
  font-family: var(--rio-font-mono);
  font-size: 0.92em;
  color: var(--rio-text-muted);
}
.rio-confirm__small {
  margin: 0 0 var(--rio-s4);
  font-size: var(--rio-fs-sm);
  color: var(--rio-text-muted);
}

/* Cascade items section header (within a confirm block). */
.rio-confirm__section-title {
  margin: var(--rio-s4) 0 var(--rio-s2);
  font-size: var(--rio-fs-xs);
  font-weight: var(--rio-fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--rio-text-subtle);
}