rustio-admin 0.24.0

Django Admin, but for Rust. A small, focused admin framework.
Documentation
/* ============================================================
 * rustio-admin / components / search_palette
 *
 * Global ⌘K search palette. Two surfaces:
 *
 *   1. .rio-search-trigger — the pill-shaped button in the topbar.
 *      Reads as a recessed placeholder ("Search…") with an inline
 *      ⌘K hint. The topbar is on the light `--rio-surface` since
 *      the chrome cascade narrowed to sidebar + footer only — the
 *      trigger sits one rung below the topbar surface (`--rio-bg`)
 *      so it reads as carved into the chrome rather than stuck on
 *      top of it. See the trigger section below for the full
 *      "recessed input + raised key chip" composition.
 *
 *   2. .rio-search-palette — the fixed-position modal dialog the
 *      trigger opens. Lives in admin/_topbar.html so the markup is
 *      adjacent to its trigger, but renders centered on viewport
 *      with a dimmed backdrop. JS toggles `aria-hidden`.
 *
 * No build step — vanilla CSS, all values resolve through `--rio-*`
 * tokens. Loaded after components/dropdowns.css so any cascade ties
 * fall in the palette's favour.
 * ============================================================ */

/* ---- Trigger button (lives in the topbar) ----------------------
 *
 * Reads as a recessed search field, not a button. Background sits
 * one rung below the topbar surface (`--rio-bg` against the topbar's
 * `--rio-surface`) so the trigger looks "carved into" the chrome
 * rather than stuck on top of it. The `⌘K` chip is a half-rung
 * lighter — looks like a raised key against the recessed field.
 * GitHub / Linear / Vercel pattern. */

.rio-search-trigger {
  /* v0.16 — 40px tall (was 34) so it sits comfortably on the 72px
   * topbar. Wider minimum (300px) and 16px text — search reads as
   * the primary affordance in the topbar instead of a cramped pill. */
  display: inline-flex;
  align-items: center;
  gap: var(--rio-s2);
  height: 40px;
  min-width: 300px;
  padding: 0 var(--rio-s3);
  background: var(--rio-bg);
  border: 1px solid var(--rio-border);
  border-radius: var(--rio-radius-sm);
  color: var(--rio-text-muted);
  font-size: var(--rio-fs-md);                 /* 16px */
  font-weight: var(--rio-fw-regular);
  cursor: pointer;
  transition: background-color 0.12s, border-color 0.12s, color 0.12s,
              box-shadow 0.12s;
}
.rio-search-trigger:hover {
  background: var(--rio-accent-soft);
  border-color: var(--rio-accent-border);
  color: var(--rio-accent-hover);
}
.rio-search-trigger:focus-visible {
  outline: none;
  border-color: var(--rio-accent);
  box-shadow: 0 0 0 4px var(--rio-accent-border);
}
.rio-search-trigger__icon {
  flex-shrink: 0;
  width: 18px;
  height: 18px;
  color: var(--rio-text-subtle);
}
.rio-search-trigger__label {
  flex: 1;
  text-align: start;
  letter-spacing: var(--rio-tracking-body);
}
.rio-search-trigger__kbd {
  display: inline-flex;
  align-items: center;
  padding: 2px 7px;
  font-family: var(--rio-font-mono);
  font-size: 11px;
  font-weight: var(--rio-fw-medium);
  background: var(--rio-surface);
  border: 1px solid var(--rio-border-soft);
  border-radius: 4px;
  color: var(--rio-text-muted);
  /* Tiny inset highlight + bottom shadow give the chip a "raised
   * key" feel against the recessed field. */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.6),
              0 1px 0 rgb(0 0 0 / 0.04);
}

/* Mobile: icon-only. The keyboard hint is meaningless on touch, and
 * the topbar runs out of horizontal room next to the brand + nav. */
@media (max-width: 767px) {
  .rio-search-trigger {
    min-width: 0;
    padding: 0 var(--rio-s2);
  }
  .rio-search-trigger__label,
  .rio-search-trigger__kbd {
    display: none;
  }
}

/* ---- Palette modal --------------------------------------------- */

.rio-search-palette {
  /* Hidden by default; JS sets aria-hidden="false" to reveal. */
  position: fixed;
  inset: 0;
  /* Top of the stacking ladder — the global search modal covers
   * topbar, sidebar, and any open dropdown. */
  z-index: var(--rio-z-modal);
  display: none;
  align-items: flex-start;
  justify-content: center;
  padding-top: 12vh;
  background: rgba(15, 23, 42, 0.45);
}
.rio-search-palette[aria-hidden="false"] {
  display: flex;
}
.rio-search-palette__dialog {
  /* v0.16 — wider dialog (720 vs 640), 12px radius matches cards.
   * The palette is one of the most-used surfaces; the extra width
   * lets long result labels breathe instead of truncating. */
  display: flex;
  flex-direction: column;
  width: min(720px, 92vw);
  max-height: 70vh;
  background: var(--rio-surface);
  border: 1px solid var(--rio-border-soft);
  border-radius: var(--rio-radius-lg);
  box-shadow: var(--rio-shadow-lg);
  overflow: hidden;
}
.rio-search-palette__input-wrap {
  /* v0.16 — 16/24 padding (was 12/16). Input area becomes the
   * dominant visual element of the palette, not an afterthought. */
  display: flex;
  align-items: center;
  gap: var(--rio-s3);
  padding: var(--rio-s4) var(--rio-s5);
  border-bottom: 1px solid var(--rio-border-soft);
  flex-shrink: 0;
}
.rio-search-palette__icon {
  flex-shrink: 0;
  width: 20px;
  height: 20px;
  color: var(--rio-text-muted);
}
.rio-search-palette__input {
  flex: 1;
  min-width: 0;
  background: transparent;
  border: 0;
  outline: none;
  font-size: var(--rio-fs-xl);                 /* 20px — search input is the page */
  font-weight: var(--rio-fw-regular);
  line-height: var(--rio-lh-ui);
  color: var(--rio-text-strong);
}
.rio-search-palette__input::placeholder {
  color: var(--rio-text-muted);
}

/* ---- Results list ---------------------------------------------- */

.rio-search-palette__list {
  list-style: none;
  margin: 0;
  padding: var(--rio-s2);
  overflow-y: auto;
  flex: 1;
}
.rio-search-palette__group + .rio-search-palette__group {
  margin-top: var(--rio-s2);
}
.rio-search-palette__group-label {
  display: block;
  padding: var(--rio-s2) var(--rio-s3) var(--rio-s1);
  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-search-palette__result {
  /* v0.16 — 12/16 padding (was 8/12). Results read like proper
   * clickable rows instead of cramped lines of text. ~44px tall. */
  display: flex;
  align-items: baseline;
  gap: var(--rio-s3);
  padding: var(--rio-s3) var(--rio-s4);
  border-radius: var(--rio-radius-sm);
  color: var(--rio-text);
  font-size: var(--rio-fs-md);                 /* 16px */
  line-height: var(--rio-lh-ui);
  text-decoration: none;
  cursor: pointer;
}
.rio-search-palette__result:hover,
.rio-search-palette__result.is-selected {
  background: var(--rio-accent-soft);
  color: var(--rio-accent-hover);
}
.rio-search-palette__result-label {
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.rio-search-palette__result-meta {
  flex-shrink: 0;
  font-size: var(--rio-fs-xs);
  color: var(--rio-text-muted);
}

/* ---- Empty / hint states --------------------------------------- */

.rio-search-palette__empty {
  padding: var(--rio-s5) var(--rio-s4);
  color: var(--rio-text-muted);
  text-align: center;
  font-size: var(--rio-fs-sm);
}

/* ---- Footer hint strip ----------------------------------------- */

.rio-search-palette__footer {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--rio-s3);
  padding: var(--rio-s2) var(--rio-s4);
  border-top: 1px solid var(--rio-border-soft);
  background: var(--rio-surface-2);
  font-size: var(--rio-fs-xs);
  color: var(--rio-text-muted);
  flex-shrink: 0;
}
.rio-search-palette__footer-item {
  display: inline-flex;
  align-items: center;
  gap: var(--rio-s1);
}
.rio-search-palette__footer-kbd {
  display: inline-flex;
  align-items: center;
  padding: 1px 5px;
  font-family: var(--rio-font-mono);
  font-size: 11px;
  border: 1px solid var(--rio-border);
  border-radius: 3px;
  background: var(--rio-surface);
  color: var(--rio-text-muted);
}