{#
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 %}