{% comment %} Server-driven tabs. Each tab button carries `dj-click="set_tab"` with its index as `data-value`, so the active tab lives in the host LiveView's state and the markup below is re-rendered from it. The `data-theme-*` attributes below are the client fallback, not the primary path. `components.js` stands down on any page carrying a djust mount root, so a LiveView page is driven entirely by `dj-click`; a plain Django page — the theme gallery, the theme editor — has no server to dispatch to and keeps using them, which is why both sets of hooks coexist here. Removing the fallback outright was tried and reverted: the theming gallery cannot be a LiveView (all 25 `theme_*` tags are registered with Django's engine only, not the Rust one), so without these it would have had no working tab set at all. The fallback is genuinely second-best for the reasons that motivated the server-driven path: the server already computed `aria-selected`, `tabindex` and the active class correctly for the initial paint, so the JS existed only to re-derive what the server had already decided. The list comparison is deliberately stringified: the DEP-002 `Tabs` descriptor stores `active` as the string the client sends, while `{% theme_tabs %}` is called with an int index. Comparing `forloop.counter0 == active` directly would be False for `0 == "0"` — a live tab set that renders tab 1 active and never moves. {% endcomment %}
{% for tab in tabs %} {% endfor %}
{% for tab in tabs %}
{{ tab.content|safe }}
{% endfor %}