dashboard: группировка виджетов по ширине (½ / полная)

Каждому виджету можно задать ширину: «Полная» (на всю строку) или «½».
Виджеты выстраиваются в flex-сетку: два «½» встают в один ряд, «Полная»
занимает строку целиком. На узких экранах половинные переносятся в столбик.

- DashboardLayout.widget_widths (JSON {ключ: half|full}) + миграция 0022;
  поле widgets не тронуто — обратная совместимость сохранена
- core/dashboard: layout_widths, ширина в build_dashboard_context/
  customize_rows, save_layout(widths=...), normalize_width
- dashboard.html: flex-wrap сетка по ширине; customize: переключатель ½/полная
- tests: 6 (сохранение/чтение ширин, нормализация, дефолт, контекст, POST)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-09-22 10:47:14 +03:00
parent 0057f367a2
commit 3387418332
7 changed files with 121 additions and 11 deletions
+5 -3
View File
@@ -9,11 +9,13 @@
</button>
</div>
<div style="display: flex; flex-direction: column; gap: 1.5rem;">
<div style="display: flex; flex-wrap: wrap; gap: 1.5rem; align-items: flex-start;">
{% for w in dashboard_widgets %}
{% include w.template %}
<div style="{% if w.width == 'half' %}flex: 1 1 calc(50% - 0.75rem); min-width: min(100%, 22rem);{% else %}flex: 1 1 100%; min-width: 0;{% endif %}">
{% include w.template %}
</div>
{% empty %}
<div style="padding: 2rem; text-align: center; color: var(--color-muted-foreground); font-size: 0.875rem;">
<div style="flex: 1 1 100%; padding: 2rem; text-align: center; color: var(--color-muted-foreground); font-size: 0.875rem;">
{% trans "Все виджеты скрыты." %}
<a href="/dashboard/customize/" hx-get="/dashboard/customize/" hx-target="#main-content" hx-push-url="/dashboard/customize/" hx-indicator="#loader" style="color: var(--color-primary);">{% trans "Настроить дашборд" %}</a>
</div>
+9 -1
View File
@@ -4,7 +4,7 @@
<div style="display: flex; align-items: flex-start; justify-content: space-between; gap: 1rem; margin-bottom: 1.25rem;">
<div>
<h1 style="font-size: 1.5rem; font-weight: 700; color: var(--color-foreground); margin-bottom: 0.25rem;">{% trans "Настройка дашборда" %}</h1>
<p style="font-size: 0.875rem; color: var(--color-muted-foreground);">{% trans "Включите нужные виджеты и расположите их в удобном порядке." %}</p>
<p style="font-size: 0.875rem; color: var(--color-muted-foreground);">{% trans "Включите нужные виджеты, задайте ширину и порядок. Два виджета «½» встают в один ряд, «Полная» — на всю ширину." %}</p>
</div>
<button class="btn btn-secondary" style="border: 1px solid var(--color-border); font-size: 0.8125rem; white-space: nowrap;"
hx-get="/dashboard/" hx-target="#main-content" hx-push-url="/dashboard/" hx-indicator="#loader">← {% trans "К дашборду" %}</button>
@@ -27,6 +27,14 @@
<span style="display: block; font-size: 0.8125rem; color: var(--color-muted-foreground); margin-top: 0.125rem;">{{ row.widget.description }}</span>
</span>
</label>
<div style="display: inline-flex; flex-shrink: 0; border: 1px solid var(--color-border); border-radius: 0.375rem; overflow: hidden; font-size: 0.75rem;">
<label style="padding: 0.25rem 0.5rem; cursor: pointer; color: var(--color-muted-foreground);">
<input type="radio" name="width_{{ row.widget.key }}" value="full" {% if row.width == 'full' %}checked{% endif %} style="margin-right: 0.2rem;">{% trans "Полная" %}
</label>
<label style="padding: 0.25rem 0.5rem; cursor: pointer; color: var(--color-muted-foreground); border-left: 1px solid var(--color-border);">
<input type="radio" name="width_{{ row.widget.key }}" value="half" {% if row.width == 'half' %}checked{% endif %} style="margin-right: 0.2rem;">½
</label>
</div>
<div style="display: flex; gap: 0.25rem; flex-shrink: 0;">
<button type="button" class="btn btn-ghost btn-icon" title="{% trans 'Выше' %}" onclick="dashMove(this, -1)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m18 15-6-6-6 6"/></svg>