umbral-admin 0.0.12

Auto-generated CRUD admin UI for umbral models.
Documentation
{#
  widget_grid.html — the dashboard widget grid, shared by dashboard.html
  and custom_view.html so custom pages render cards/charts identically.
  Each cell self-loads from {admin_base}/api/dashboard/widgets/{key}/data.

  Params:
    widget_sections : Vec of { title, subtitle, widgets: [{key,title,kind,span:{cols,rows}}] }
    admin_base      : the admin base path
#}
{% macro widget_grid(widget_sections, admin_base) %}
{% for section in widget_sections %}
{% if section.widgets and section.widgets | length > 0 %}
<section class="mb-xl">
  <div class="flex items-end justify-between mb-md gap-md">
    <div class="min-w-0">
      <h2 class="font-h3 text-h3 text-on-surface">{{ section.title }}</h2>
      {% if section.subtitle %}
      <p class="text-label-sm text-outline mt-xs truncate">{{ section.subtitle }}</p>
      {% endif %}
    </div>
    <span class="text-label-sm text-outline flex-shrink-0">{{ section.widgets | length }} active</span>
  </div>
  {# `data-widget-grid` marks this as a drag-reorder surface; admin.js makes the
     cells draggable and PUTs the new order to /api/dashboard/layout. #}
  <div class="grid grid-cols-12 gap-sm auto-rows-[120px]" data-widget-grid>
    {% for w in section.widgets %}
    {%- set col_span = w.span.cols if w.span and w.span.cols else 3 -%}
    {%- set row_span = w.span.rows if w.span and w.span.rows else 1 -%}
    {# Grid span is a DATA-DRIVEN per-widget value, so it lives in an
       inline style, not a `col-span-N` utility class — Tailwind purges
       interpolated class names (`col-span-{{ ... }}`), so those classes
       never made it into admin.css and every widget collapsed to one
       column. Inline grid styles are purge-proof and need no rebuild. #}
    <div
      class="bg-surface border border-outline-variant rounded-xl shadow-card overflow-hidden"
      style="grid-column: span {{ col_span }} / span {{ col_span }}; grid-row: span {{ row_span }} / span {{ row_span }};"
      data-widget-kind="{{ w.kind }}"
      data-widget-key="{{ w.key }}"
      data-span-cols="{{ col_span }}"
      data-span-rows="{{ row_span }}"
      id="widget-{{ w.key }}"
      hx-get="{{ admin_base }}/api/dashboard/widgets/{{ w.key }}/data"
      hx-trigger="load"
      hx-swap="innerHTML"
      hx-target="#widget-{{ w.key }}"
    >
      {# Per-kind loading skeleton — matches the real widget's shape #}
      {% if w.kind == "kpi" or w.kind == "card" %}
      <div class="h-full p-lg flex flex-col justify-between animate-pulse">
        <div class="flex items-start justify-between gap-sm">
          <div class="h-3 bg-surface-container-high rounded w-1/3"></div>
          <div class="w-8 h-8 rounded-lg bg-surface-container-low border border-divider-soft"></div>
        </div>
        <div class="mt-auto">
          <div class="h-9 bg-surface-container-high rounded w-1/2"></div>
          <div class="h-3 bg-surface-container-high rounded w-1/4 mt-sm"></div>
        </div>
      </div>
      {% elif w.kind in ["line", "bar"] %}
      <div class="h-full p-md flex flex-col animate-pulse">
        <div class="h-3 bg-surface-container-high rounded w-1/3 flex-shrink-0 mb-sm"></div>
        <div class="flex-1 bg-surface-container-high rounded"></div>
      </div>
      {% elif w.kind == "donut" %}
      <div class="h-full p-md flex flex-col animate-pulse">
        <div class="h-3 bg-surface-container-high rounded w-1/3 flex-shrink-0 mb-md"></div>
        <div class="flex-1 flex items-center justify-center">
          <div class="w-24 h-24 rounded-full border-[14px] border-surface-container-high"></div>
        </div>
      </div>
      {% elif w.kind == "radial" %}
      <div class="h-full p-md flex flex-col animate-pulse">
        <div class="h-3 bg-surface-container-high rounded w-1/3 flex-shrink-0 mb-md"></div>
        <div class="flex-1 flex items-center justify-center">
          <div class="w-24 h-24 rounded-full border-[10px] border-surface-container-high"></div>
        </div>
      </div>
      {% elif w.kind == "heatmap" %}
      <div class="h-full p-md flex flex-col animate-pulse">
        <div class="h-3 bg-surface-container-high rounded w-1/3 flex-shrink-0 mb-md"></div>
        <div class="flex-1 grid grid-cols-6 grid-rows-4 gap-1">
          {% for i in range(24) %}<div class="bg-surface-container-high rounded-sm"></div>{% endfor %}
        </div>
      </div>
      {% elif w.kind == "progress" %}
      <div class="h-full p-md flex flex-col gap-sm animate-pulse">
        <div class="h-3 bg-surface-container-high rounded w-1/3 flex-shrink-0 mb-xs"></div>
        <div class="space-y-xs">
          <div class="h-2 bg-surface-container-high rounded w-1/4"></div>
          <div class="h-2 bg-surface-container-high rounded-full w-full"></div>
        </div>
        <div class="space-y-xs">
          <div class="h-2 bg-surface-container-high rounded w-1/3"></div>
          <div class="h-2 bg-surface-container-high rounded-full w-3/4"></div>
        </div>
        <div class="space-y-xs">
          <div class="h-2 bg-surface-container-high rounded w-1/4"></div>
          <div class="h-2 bg-surface-container-high rounded-full w-1/2"></div>
        </div>
        <div class="space-y-xs">
          <div class="h-2 bg-surface-container-high rounded w-1/3"></div>
          <div class="h-2 bg-surface-container-high rounded-full w-1/3"></div>
        </div>
      </div>
      {% elif w.kind == "feed" %}
      <div class="h-full p-md flex flex-col animate-pulse">
        <div class="h-3 bg-surface-container-high rounded w-1/3 flex-shrink-0 mb-sm"></div>
        <div class="flex-1 space-y-sm">
          <div class="flex items-start gap-sm">
            <div class="w-7 h-7 rounded-full bg-surface-container-high flex-shrink-0"></div>
            <div class="flex-1 space-y-xs">
              <div class="h-3 bg-surface-container-high rounded w-3/4"></div>
              <div class="h-2 bg-surface-container-high rounded w-1/4"></div>
            </div>
          </div>
          <div class="flex items-start gap-sm">
            <div class="w-7 h-7 rounded-full bg-surface-container-high flex-shrink-0"></div>
            <div class="flex-1 space-y-xs">
              <div class="h-3 bg-surface-container-high rounded w-2/3"></div>
              <div class="h-2 bg-surface-container-high rounded w-1/3"></div>
            </div>
          </div>
          <div class="flex items-start gap-sm">
            <div class="w-7 h-7 rounded-full bg-surface-container-high flex-shrink-0"></div>
            <div class="flex-1 space-y-xs">
              <div class="h-3 bg-surface-container-high rounded w-3/4"></div>
              <div class="h-2 bg-surface-container-high rounded w-1/4"></div>
            </div>
          </div>
        </div>
      </div>
      {% elif w.kind == "table" %}
      <div class="h-full flex flex-col animate-pulse">
        <div class="px-md pt-md pb-sm">
          <div class="h-3 bg-surface-container-high rounded w-1/4"></div>
        </div>
        <div class="flex-1 px-md pb-md space-y-xs">
          <div class="h-6 bg-surface-container-high rounded"></div>
          <div class="h-6 bg-surface-container-high rounded opacity-60"></div>
          <div class="h-6 bg-surface-container-high rounded opacity-40"></div>
        </div>
      </div>
      {% else %}
      <div class="h-full p-md flex flex-col gap-sm animate-pulse">
        <div class="h-3 bg-surface-container-high rounded w-1/3"></div>
        <div class="flex-1 bg-surface-container-high rounded"></div>
      </div>
      {% endif %}
    </div>
    {% endfor %}
  </div>
</section>
{% endif %}
{% endfor %}
{% endmacro %}