rustio-admin 0.2.0

Django Admin, but for Rust. A small, focused admin framework.
Documentation
{% extends "admin/_base.html" %}
{% block content %}
<header class="rio-page-header">
  <nav class="rio-breadcrumbs"><a href="/admin">Home</a> · <span>{{ display_name }}</span></nav>
  <div class="rio-page-actions">
    <h1>{{ display_name }}</h1>
    <a class="rio-button rio-button--primary" href="/admin/{{ admin_name }}/new">{{ icon("plus", class="rio-icon") }} Add {{ singular_name }}</a>
  </div>
</header>

<div class="rio-toolbar">
  {# The search form carries hidden inputs for every active filter +
   # the current sort, so submitting a query keeps the rest of the
   # list-view state intact. Page intentionally resets to 1 — a new
   # query against page N rarely makes sense. #}
  <form method="get" action="/admin/{{ admin_name }}" class="rio-search-bar" role="search">
    <span class="rio-search-input-wrap">
      {{ icon("search", class="rio-search-icon") }}
      <input type="search" name="q" value="{{ search_query }}" placeholder="Search…" class="rio-search-input" aria-label="Search">
    </span>
    {% for pair in active_filter_pairs %}
    <input type="hidden" name="{{ pair[0] }}" value="{{ pair[1] }}">
    {% endfor %}
    {% if active_sort_field %}
    <input type="hidden" name="sort" value="{{ active_sort_field }}">
    <input type="hidden" name="dir" value="{{ active_sort_dir }}">
    {% endif %}
    {% if active_per_page_override %}
    <input type="hidden" name="per_page" value="{{ active_per_page_override }}">
    {% endif %}
    <button type="submit" class="rio-button">Search</button>
  </form>

  {% if filters %}
  <div class="rio-dropdown" data-rio-dropdown>
    <button type="button" class="rio-dropdown-toggle" aria-haspopup="true" aria-expanded="false">
      {{ icon("filter", class="rio-icon") }}
      Filters
      {% if active_filter_count > 0 %}
      <span class="rio-dropdown-badge" aria-label="{{ active_filter_count }} active filter">{{ active_filter_count }}</span>
      {% endif %}
      {{ icon("chevron-down", class="rio-chev") }}
    </button>
    <div class="rio-dropdown-panel" role="dialog" aria-label="Filters">
      {% for group in filters %}
      <div class="rio-dropdown-section">
        <span class="rio-dropdown-label">{{ group.label }}</span>
        <div class="rio-dropdown-options">
          <a href="{{ group.all_link }}" class="rio-dropdown-chip{% if not group.current %} is-active{% endif %}">All</a>
          {% for opt in group.options %}
          <a href="{{ opt.link }}" class="rio-dropdown-chip{% if opt.selected %} is-active{% endif %}">{{ opt.label }}</a>
          {% endfor %}
        </div>
      </div>
      {% endfor %}
      {% if active_filter_count > 0 %}
      <div class="rio-dropdown-footer">
        <a href="{{ clear_all_filters_link }}" class="rio-button">Clear all</a>
      </div>
      {% endif %}
    </div>
  </div>
  {% endif %}

  {% if sort_options %}
  <div class="rio-dropdown" data-rio-dropdown>
    <button type="button" class="rio-dropdown-toggle" aria-haspopup="true" aria-expanded="false">
      Sort: <strong style="font-weight: 600; color: var(--rio-text-strong);">{{ current_sort_label }}</strong>
      {{ icon("chevron-down", class="rio-chev") }}
    </button>
    <div class="rio-dropdown-panel" role="dialog" aria-label="Sort">
      <div class="rio-dropdown-menu">
        {% for opt in sort_options %}
        <a href="{{ opt.link }}" class="rio-dropdown-item{% if opt.is_active %} is-active{% endif %}">{{ opt.label }}</a>
        {% endfor %}
      </div>
    </div>
  </div>
  {% endif %}

  {% if per_page_options %}
  <div class="rio-dropdown" data-rio-dropdown>
    <button type="button" class="rio-dropdown-toggle" aria-haspopup="true" aria-expanded="false">
      {{ current_per_page_label }}
      {{ icon("chevron-down", class="rio-chev") }}
    </button>
    <div class="rio-dropdown-panel" role="dialog" aria-label="Rows per page">
      <div class="rio-dropdown-menu">
        {% for opt in per_page_options %}
        <a href="{{ opt.link }}" class="rio-dropdown-item{% if opt.is_active %} is-active{% endif %}">{{ opt.label }}</a>
        {% endfor %}
      </div>
    </div>
  </div>
  {% endif %}
</div>

{% if active_filter_pills %}
<div class="rio-active-filters" role="region" aria-label="Active filters">
  <span class="rio-active-filters-label">Active</span>
  {% for pill in active_filter_pills %}
  <span class="rio-active-pill">
    <span class="rio-active-pill-key">{{ pill.label }}:</span>
    {{ pill.value_label }}
    <a href="{{ pill.remove_link }}" class="rio-active-pill-x" aria-label="Remove {{ pill.label }} filter">×</a>
  </span>
  {% endfor %}
  <a href="{{ clear_all_filters_link }}" class="rio-active-filters-clear">Clear all</a>
</div>
{% endif %}

{# Bulk-select form wraps the entire table so checked rows submit as
 # a single comma-separated `_ids` field built by admin.js. The form
 # POSTs to `/admin/{name}/bulk_delete`; the server's two-step flow
 # (confirm → commit) takes over from there. JS toggles `.is-active`
 # on the form when ≥1 checkbox is checked, which reveals the bulk
 # bar. Without JS, the bulk bar stays hidden and users fall back to
 # per-row Delete actions. #}
<form method="post" action="/admin/{{ admin_name }}/bulk_delete" class="rio-bulk-form" data-rio-bulk>
<input type="hidden" name="_csrf" value="{{ csrf_token }}">
<input type="hidden" name="_ids" value="" data-rio-bulk-ids>

<div class="rio-bulk-bar" role="region" aria-live="polite" aria-label="Bulk actions">
  <span class="rio-bulk-bar-count"><strong data-rio-bulk-count>0</strong> selected</span>
  {# Built-in Delete uses the form's default action (/bulk_delete).
   # Project-defined actions override via `formaction`, so the
   # same form + same selected ids dispatch to different routes. #}
  <button type="submit" class="rio-button rio-button--danger">{{ icon("trash", class="rio-icon") }} Delete selected</button>
  {% for btn in bulk_action_buttons %}
  <button type="submit" formaction="{{ btn.form_action }}" class="rio-button{% if btn.destructive %} rio-button--danger{% endif %}">{{ btn.label }}</button>
  {% endfor %}
  <button type="button" class="rio-bulk-bar-clear" data-rio-bulk-clear>Clear selection</button>
</div>

<section class="rio-card rio-list">
  {% if rows %}
  <table class="rio-table rio-table--striped">
    <thead>
      <tr>
        <th class="rio-th rio-th--checkbox">
          <input type="checkbox" class="rio-row-checkbox-all" data-rio-bulk-all aria-label="Select all rows">
        </th>
        {% for f in fields %}
        <th class="rio-th rio-th--{{ f.kind }}{% if f.sort_active %} rio-th--sort-{{ f.sort_active }}{% endif %}">
          <a href="{{ f.sort_link }}" class="rio-th-sort">
            {{ f.label }}{% if f.sort_active == "asc" %} ▲{% elif f.sort_active == "desc" %} ▼{% endif %}
          </a>
        </th>
        {% endfor %}
        <th class="rio-th rio-th--actions">Actions</th>
      </tr>
    </thead>
    <tbody>
      {% for row in rows %}
      <tr>
        <td class="rio-td rio-td--checkbox">
          <input type="checkbox" class="rio-row-checkbox" data-rio-bulk-row value="{{ row.id }}" aria-label="Select row {{ row.id }}">
        </td>
        {% for f in fields %}
        <td class="rio-td rio-td--{{ f.kind }}"{% if f.kind == "text" %} title="{{ row[f.name]|default(value='') }}"{% endif %}>
          {% if f.name == "id" %}
            <a href="/admin/{{ admin_name }}/{{ row.id }}/edit">{{ row.id }}</a>
          {% elif f.kind == "checkbox" %}
            {% if row[f.name] %}Yes{% else %}No{% endif %}
          {% else %}
            {{ row[f.name]|default(value='') }}
          {% endif %}
        </td>
        {% endfor %}
        <td class="rio-td rio-td--actions">
          <a class="rio-button rio-button--ghost rio-button--sm" href="/admin/{{ admin_name }}/{{ row.id }}/edit">{{ icon("pencil", class="rio-icon") }} Edit</a>
          <a class="rio-button rio-button--danger-ghost rio-button--sm" href="/admin/{{ admin_name }}/{{ row.id }}/delete">{{ icon("trash", class="rio-icon") }} Delete</a>
        </td>
      </tr>
      {% endfor %}
    </tbody>
  </table>
  {% else %}
  <p class="rio-empty">No {{ display_name|lower }} yet. <a href="/admin/{{ admin_name }}/new">Add the first one.</a></p>
  {% endif %}
</section>

</form>

{% if total_pages > 1 %}
<nav class="rio-pagination" aria-label="Pagination">
  <span class="rio-meta">{{ total_rows }} total · page {{ page }} of {{ total_pages }}</span>
  {% if prev_page_link %}<a class="rio-pagination-link" href="{{ prev_page_link }}">← Previous</a>{% endif %}
  {% for item in page_items %}
    {% if item.kind == "ellipsis" %}
      <span class="rio-pagination-ellipsis" aria-hidden="true"></span>
    {% elif item.is_active %}
      <span class="rio-pagination-num is-active" aria-current="page">{{ item.number }}</span>
    {% else %}
      <a class="rio-pagination-num" href="{{ item.link }}">{{ item.number }}</a>
    {% endif %}
  {% endfor %}
  {% if next_page_link %}<a class="rio-pagination-link" href="{{ next_page_link }}">Next →</a>{% endif %}
</nav>
{% endif %}
{% endblock %}