rustio-admin 0.18.5

Django Admin, but for Rust. A small, focused admin framework.
Documentation
/* ============================================================
 * rustio-admin / components / dropdowns
 *
 * Generic primitive — used today by the Filters dropdown on the
 * list view. Sort menus, per-page pickers, and any future "click
 * button → floating panel" widget should reuse the same
 * `.rio-dropdown` machinery. JS hook lives in admin.js
 * (`[data-rio-dropdown]`) and just toggles `.is-open` on the
 * wrapper.
 * ============================================================ */

.rio-dropdown { position: relative; display: inline-block; }

.rio-dropdown-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  padding: 0.55rem var(--rio-s3) 0.55rem var(--rio-s4);
  background: var(--rio-surface);
  color: var(--rio-text);
  border: 1px solid var(--rio-border);
  border-radius: var(--rio-radius-sm);
  font: inherit;
  font-size: var(--rio-fs-md);
  font-weight: var(--rio-fw-semibold);
  line-height: var(--rio-lh-ui);
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.rio-dropdown-toggle:hover {
  background: var(--rio-surface-3);
  border-color: var(--rio-border-strong);
  color: var(--rio-text-strong);
}
.rio-dropdown-toggle .rio-icon { width: 16px; height: 16px; opacity: 0.85; }
.rio-dropdown-toggle .rio-chev {
  width: 13px; height: 13px;
  opacity: 0.55;
  transition: transform 0.18s;
}
.rio-dropdown.is-open .rio-dropdown-toggle {
  background: var(--rio-surface-3);
  border-color: var(--rio-border-strong);
  color: var(--rio-text-strong);
}
.rio-dropdown.is-open .rio-dropdown-toggle .rio-chev { transform: rotate(180deg); }

/* Active-filter count badge — small accent pill on the toggle. */
.rio-dropdown-badge {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 18px; height: 18px; padding: 0 6px;
  background: var(--rio-accent);
  color: #fff;
  font-size: 11px;
  font-weight: var(--rio-fw-bold);
  border-radius: 9px;
  font-variant-numeric: tabular-nums;
}

/* Floating panel — anchored to the toggle's end-side edge so it
 * doesn't push past the viewport on a wide toolbar. Under RTL the
 * panel anchors to the left edge of the toggle (which is the
 * inline-end in that direction), preserving the same "stays inside
 * the toolbar" behaviour. JS toggles `.is-open`. */
.rio-dropdown-panel {
  display: none;
  position: absolute;
  top: calc(100% + 6px);
  inset-inline-end: 0;
  min-width: 320px;
  max-width: min(420px, calc(100vw - var(--rio-s5)));
  background: var(--rio-surface);
  border: 1px solid var(--rio-border);
  border-radius: var(--rio-radius);
  box-shadow: var(--rio-shadow-lg);
  padding: var(--rio-s4);
  z-index: 40;
}
.rio-dropdown.is-open .rio-dropdown-panel { display: block; }

.rio-dropdown-section { margin-bottom: var(--rio-s4); }
.rio-dropdown-section:last-of-type { margin-bottom: var(--rio-s3); }
.rio-dropdown-label {
  display: block;
  font-size: var(--rio-fs-xs);
  font-weight: var(--rio-fw-bold);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--rio-text-subtle);
  margin-bottom: var(--rio-s2);
}
.rio-dropdown-options {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
}

/* Dropdown chip — anchor that navigates on click (each chip is a
 * `<a href="?field=value">`). The framework's filters are URL-driven,
 * so no Apply step is needed: clicking a chip commits the filter. */
.rio-dropdown-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.3rem 0.7rem;
  background: var(--rio-surface-2);
  color: var(--rio-text);
  border: 1px solid var(--rio-border);
  border-radius: 14px;
  font-size: var(--rio-fs-sm);
  font-weight: var(--rio-fw-semibold);
  line-height: var(--rio-lh-ui);
  transition: background 0.12s, border-color 0.12s, color 0.12s;
}
.rio-dropdown-chip:hover {
  text-decoration: none;
  background: var(--rio-surface-3);
  border-color: var(--rio-border-strong);
  color: var(--rio-text-strong);
}
.rio-dropdown-chip.is-active,
.rio-dropdown-chip.is-active:hover {
  background: rgb(var(--rio-accent-rgb) / 0.10);
  color: var(--rio-accent);
  border-color: rgb(var(--rio-accent-rgb) / 0.30);
}

/* Vertical menu — full-width clickable rows. Used by the toolbar's
 * Sort dropdown (and any future per-page picker / column toggler).
 * Each item is an `<a>` so navigation stays the source of truth. */
.rio-dropdown-menu {
  display: flex;
  flex-direction: column;
  margin: calc(-0.4rem) calc(-0.4rem) 0;
}
.rio-dropdown-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--rio-s3);
  padding: 0.5rem 0.75rem;
  border-radius: var(--rio-radius-sm);
  color: var(--rio-text);
  font-size: var(--rio-fs-md);
  font-weight: var(--rio-fw-medium);
  line-height: var(--rio-lh-ui);
  white-space: nowrap;
  transition: background 0.1s, color 0.1s;
}
.rio-dropdown-item:hover {
  text-decoration: none;
  background: var(--rio-surface-3);
  color: var(--rio-text-strong);
}
.rio-dropdown-item.is-active,
.rio-dropdown-item.is-active:hover {
  background: rgb(var(--rio-accent-rgb) / 0.10);
  color: var(--rio-accent);
  font-weight: var(--rio-fw-semibold);
}
/* Active row trails a subtle bullet — the cleanest "current selection"
 * marker that doesn't fight the accent text colour. */
.rio-dropdown-item.is-active::after {
  content: "";
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--rio-accent);
  flex-shrink: 0;
}

/* File-input menu row — used by the list-page "View" dropdown's
 * "Import CSV…" entry. The dropdown row IS the file picker:
 * the <label> wraps a hidden <input type=file>, so clicking
 * anywhere on the row opens the OS picker. `tabindex=0` keeps
 * it keyboard-reachable. Style matches a regular menu item but
 * justify-content stays start (the row carries an icon + label,
 * no trailing affordance). */
.rio-dropdown-item--file {
  justify-content: flex-start;
  gap: var(--rio-s2);
  cursor: pointer;
  /* Reset the form's default margin so the row aligns with
   * sibling .rio-dropdown-item entries above it. */
  margin: 0;
}
.rio-dropdown-item--file:focus-visible {
  outline: 2px solid var(--rio-accent);
  outline-offset: -2px;
}
/* The inline import form wrapping the label shouldn't add its
 * own layout — the label IS the visible row. */
.rio-dropdown-menu > .rio-toolbar-import {
  margin: 0;
}

.rio-dropdown-footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--rio-s2);
  padding-top: var(--rio-s3);
  margin-top: var(--rio-s2);
  border-top: 1px solid var(--rio-border-soft);
}
.rio-dropdown-footer .rio-button { padding: 0.4rem 0.85rem; font-size: var(--rio-fs-sm); }

/* Date-range filter form. Lives inside `.rio-dropdown-section` for
 * any FilterKind::DateRange field — two stacked `<input type=date>`
 * labels and a small button row. The native date picker carries
 * its own visual styling; the form just establishes spacing. */
.rio-date-range {
  display: flex;
  flex-direction: column;
  gap: var(--rio-s2);
}
.rio-date-range-field {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.rio-date-range-label-text {
  font-size: var(--rio-fs-xs);
  font-weight: var(--rio-fw-semibold);
  color: var(--rio-text-subtle);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.rio-date-range-input { padding: 0.35rem 0.5rem; font-size: var(--rio-fs-sm); }
.rio-date-range-actions {
  display: flex;
  gap: var(--rio-s2);
  padding-top: var(--rio-s1);
}
.rio-date-range-actions .rio-button { padding: 0.4rem 0.85rem; font-size: var(--rio-fs-sm); }

/* Multi-select filter form. Lives inside `.rio-dropdown-section` for
 * any FilterKind::MultiSelect field — a vertical checkbox list and a
 * small button row. Each row hugs the label so the click target
 * extends across the whole option. */
.rio-multi-select {
  display: flex;
  flex-direction: column;
  gap: var(--rio-s2);
}
.rio-multi-select-options {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}
.rio-multi-select-option {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.3rem 0.4rem;
  border-radius: var(--rio-radius-sm);
  font-size: var(--rio-fs-sm);
  cursor: pointer;
  transition: background 0.1s;
}
.rio-multi-select-option:hover { background: var(--rio-surface-3); }
.rio-multi-select-actions {
  display: flex;
  gap: var(--rio-s2);
  padding-top: var(--rio-s1);
}
.rio-multi-select-actions .rio-button { padding: 0.4rem 0.85rem; font-size: var(--rio-fs-sm); }

/* FK-autocomplete filter form. A search input plus a JS-populated
 * results list that floats just below the input. The wrapper is
 * `position: relative` so the absolute-positioned results panel
 * anchors to it. Without JS the input is inert as a search widget,
 * but the form still submits whatever id the operator types. */
.rio-fk-autocomplete {
  display: flex;
  flex-direction: column;
  gap: var(--rio-s2);
}
.rio-fk-autocomplete-wrap {
  position: relative;
}
.rio-fk-autocomplete-input {
  width: 100%;
  padding: 0.35rem 0.5rem;
  font-size: var(--rio-fs-sm);
}
.rio-fk-autocomplete-results {
  position: absolute;
  top: calc(100% + 0.2rem);
  inset-inline-start: 0;
  inset-inline-end: 0;
  list-style: none;
  margin: 0;
  padding: 0.2rem;
  background: var(--rio-surface-elevated, var(--rio-surface));
  border: 1px solid var(--rio-border);
  border-radius: var(--rio-radius-sm);
  box-shadow: var(--rio-shadow-2, 0 4px 16px rgb(0 0 0 / 0.12));
  max-height: 240px;
  overflow-y: auto;
  z-index: 50;
}
.rio-fk-autocomplete-results[hidden] { display: none; }
.rio-fk-autocomplete-result {
  padding: 0.35rem 0.5rem;
  border-radius: var(--rio-radius-sm);
  font-size: var(--rio-fs-sm);
  cursor: pointer;
}
.rio-fk-autocomplete-result:hover { background: var(--rio-surface-3); }
.rio-fk-autocomplete-empty {
  padding: 0.35rem 0.5rem;
  font-size: var(--rio-fs-sm);
  color: var(--rio-text-subtle);
  font-style: italic;
}
.rio-fk-autocomplete-actions {
  display: flex;
  gap: var(--rio-s2);
}
.rio-fk-autocomplete-actions .rio-button { padding: 0.4rem 0.85rem; font-size: var(--rio-fs-sm); }

/* Saved-filters dropdown — per-operator list-page bookmarks.
 * Each row is "<apply link> <delete ×>" with the active preset
 * highlighted in accent. The "Save current view" form lives in a
 * sibling section below the list. */
.rio-saved-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}
.rio-saved-list__row {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.25rem 0.35rem;
  border-radius: var(--rio-radius-sm);
  font-size: var(--rio-fs-sm);
}
.rio-saved-list__row:hover { background: var(--rio-surface-3); }
.rio-saved-list__row.is-active {
  background: rgb(var(--rio-accent-rgb) / 0.10);
  color: var(--rio-accent);
}
.rio-saved-list__apply {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: inherit;
}
.rio-saved-list__apply:hover { text-decoration: none; color: var(--rio-text-strong); }
.rio-saved-list__delete { margin: 0; display: inline-flex; }
.rio-saved-list__delete-btn {
  background: none;
  border: none;
  color: var(--rio-text-subtle);
  cursor: pointer;
  font-size: 1.1em;
  line-height: 1;
  padding: 0 0.3rem;
  border-radius: var(--rio-radius-sm);
}
.rio-saved-list__delete-btn:hover { color: var(--rio-danger); background: var(--rio-surface-2); }
.rio-saved-form {
  display: flex;
  gap: var(--rio-s2);
  align-items: stretch;
}
.rio-saved-form__name {
  flex: 1;
  min-width: 0;
  padding: 0.35rem 0.5rem;
  font-size: var(--rio-fs-sm);
}