rustio-admin 0.24.0

Django Admin, but for Rust. A small, focused admin framework.
Documentation
/* ============================================================
 * rustio-admin / components / forms
 *
 * Form shell, fieldsets, fields, inputs, textareas, selects,
 * checkboxes, and the form action bar. Single-column flow by
 * default with optional `.rio-field--full` for explicit grid spans
 * inside a grid-mode fieldset.
 *
 * Table-row bulk-select checkboxes (.rio-row-checkbox) live in
 * components/tables.css since they're table-shaped controls, not
 * form-shell controls.
 * ============================================================ */

/* Editorial wrapper for form pages — caps content width so labels +
 * inputs read at a comfortable line length. Forms ship at 880px max
 * (matches Linear / Stripe / Vercel admin); page header lives inside
 * the same shell so headings, breadcrumbs, and inputs share an
 * alignment guide. */
.rio-form-shell {
  max-width: 880px;
  margin: 0 auto;
}

/* Forms stack vertically with a comfortable rhythm. `gap` on the
 * form means consecutive .rio-field / .rio-fieldset / .rio-form-actions
 * siblings get spacing automatically — no per-element margin
 * accounting and no double-margin collapse surprises. */
.rio-form {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--rio-s5);
}
.rio-fieldset {
  /* v0.16 — generous 32px internal padding on cards. Previously
   * 24/24 felt pinched; the new 32/32 pad gives form sections the
   * "premium card" feel without inflating the page. */
  border: 1px solid var(--rio-border-soft);
  border-radius: var(--rio-radius-lg);
  padding: var(--rio-s6);
  margin: 0;
  background: var(--rio-surface);
  box-shadow: var(--rio-shadow);
}
.rio-fieldset > legend {
  font-size: var(--rio-fs-xs);
  font-weight: var(--rio-fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--rio-text-subtle);
  padding: 0 var(--rio-s2);
  margin-inline-start: calc(-1 * var(--rio-s2));
}
/* Single-column field flow by default. Forms read better as a
 * vertical reading column than a 2-up grid that leaves gaps when a
 * model has 3 short fields and 1 textarea. The grid still resolves
 * `field--full` cleanly because every cell is implicitly full-width. */
.rio-fieldset-grid {
  display: flex;
  flex-direction: column;
  gap: var(--rio-s5);
}

.rio-field {
  display: flex;
  flex-direction: column;
  gap: var(--rio-s2);
}
.rio-field--full { grid-column: 1 / -1; }
.rio-field-label {
  font-weight: var(--rio-fw-semibold);
  font-size: var(--rio-fs-md);
  color: var(--rio-text-strong);
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
}
.rio-required {
  color: var(--rio-danger);
  font-size: var(--rio-fs-xs);
  font-weight: var(--rio-fw-bold);
  margin-inline-start: 0;
}
.rio-field-hint {
  margin: 0;
  font-size: var(--rio-fs-sm);
  color: var(--rio-text-muted);
  line-height: var(--rio-lh-ui);
}
.rio-field-errors {
  list-style: none;
  margin: var(--rio-s1) 0 0;
  padding: 0;
  color: var(--rio-danger);
  font-size: var(--rio-fs-sm);
}

.rio-input,
.rio-textarea,
.rio-select {
  /* v0.16 — 44px target height (touch-target standard). Padding bumped
   * to 10/14 so single-line inputs sit at exactly 44px with 16px text
   * at 1.6 line-height. Inputs read as "place to type" not "drawn
   * rectangle." */
  width: 100%;
  min-height: 44px;
  padding: 0.625rem var(--rio-s3);              /* 10px / 12px */
  border: 1px solid var(--rio-border-strong);
  border-radius: var(--rio-radius-sm);
  background: var(--rio-surface);
  color: var(--rio-text-strong);
  box-shadow: var(--rio-shadow-inset);
  font: inherit;
  font-size: var(--rio-fs-md);                  /* 16px */
  line-height: var(--rio-lh-ui);
  transition: border-color 0.12s, box-shadow 0.12s, background-color 0.12s;
}
.rio-input::placeholder,
.rio-textarea::placeholder { color: var(--rio-text-subtle); }
.rio-input:hover,
.rio-textarea:hover,
.rio-select:hover { border-color: var(--rio-text-muted); }
.rio-input:focus,
.rio-textarea:focus,
.rio-select:focus {
  outline: none;
  border-color: var(--rio-accent);
  /* v0.16 — focus ring uses the new --rio-accent-border token (teal-200)
   * as a 4px halo. Reads as "this is the focused field" from across the
   * page without flash. Inset stays so the field keeps its depth. */
  box-shadow: var(--rio-shadow-inset),
              0 0 0 4px var(--rio-accent-border);
}
.rio-input:disabled,
.rio-textarea:disabled,
.rio-select:disabled {
  background: var(--rio-surface-2);
  color: var(--rio-text-subtle);
  cursor: not-allowed;
}
.rio-textarea { font-family: var(--rio-font-sans); resize: vertical; min-height: 7rem; }

.rio-checkbox {
  display: inline-flex;
  align-items: center;
  gap: var(--rio-s2);
  cursor: pointer;
}
.rio-checkbox input[type="checkbox"] {
  width: 1rem;
  height: 1rem;
  accent-color: var(--rio-accent);
}
.rio-checkbox-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--rio-s2);
}

/* Form action bar — primary save buttons sit left, secondary
 * (History) and destructive (Delete / Cancel) push right via
 * `.rio-form-actions-spacer`. A top divider grounds the row so it
 * reads as a deliberate footer rather than buttons floating below
 * the card. */
.rio-form-actions {
  display: flex;
  align-items: center;
  gap: var(--rio-s2);
  flex-wrap: wrap;
  padding-top: var(--rio-s5);
  border-top: 1px solid var(--rio-border-soft);
}
.rio-form-actions-spacer { flex: 1; min-width: var(--rio-s4); }

/* Inline-children sections — rendered below the parent edit form
 * when a model declares ModelAdmin::inlines(). v1 is read-only:
 * each row is a click-through to the child's own edit page, with
 * "Add new …" / "View all …" affordances in the footer. */
.rio-inline-section {
  margin-top: var(--rio-s5);
}
.rio-inline-section__header {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--rio-s3);
  margin-bottom: var(--rio-s3);
}
.rio-inline-section__header h2 {
  margin: 0;
  font-size: var(--rio-fs-lg);
  font-weight: var(--rio-fw-semibold);
}
.rio-inline-table { margin-bottom: var(--rio-s3); }
.rio-inline-section__actions {
  display: flex;
  gap: var(--rio-s2);
  flex-wrap: wrap;
}