Все выпадающие списки (фильтры «Устройства», «Бэкапы», «Журнал», поле «Группа» в окне устройства) оформлены как меню «⋯» (docs/changes/020): - app.js строит меню поверх скрытого select: значение уходит с формой, без JavaScript работает стандартный список; выбор отправляет change — существующие onchange/hx-trigger не менялись; - позиционирование — существующий placeMenu, выбранный пункт отмечен «✓», клавиатура: Tab, Enter/Space, ↑/↓, Escape; - Escape при открытом списке в окне закрывает только список. Тесты: 24 из 24. Автотестов JS нет; ручная проверка UI пользователем на момент коммита не подтверждена. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7.8 KiB
План: 020 — выпадающие списки в стиле меню действий
Context
Фильтры и поле «Группа» используют стандартный <select>: раскрытый список рисует браузер/ОС, он не совпадает
с дизайном приложения (шрифт, скругления, тени, тёмная тема). Пользователь просит заменить все выпадающие списки
на оформление меню действий строки устройства («⋯»: details.menu + .menu-panel + пункты .mi, выбранный — .mi.on с «✓»).
Где сейчас <select class="field">:
dashboard.html— фильтры «Статус», «Обновления», «Канал» (форма#dev-filters,hx-trigger="input delay:300ms, change");backups.html— «Группа», «Устройство», «Тип файла» (onchange="this.form.submit()");events.html— «Тип», «Актор», «Устройство» (onchange="this.form.submit()");_device_form.html— «Группа» в окне/странице устройства (data-newgroup, пункт «+ Новая группа…» показывает поле названия; окно приходит через HTMX).
Решение (технический выбор оркестратора, UX — по запросу пользователя): прогрессивное улучшение в app.js.
Нативный <select> остаётся в разметке (значение уходит с формой, работает без JavaScript, текущая логика
syncFilters, data-newgroup, onchange/hx-trigger не меняется), скрывается, а рядом строится меню в стиле «⋯».
Изменения
app/ui/static/app.js
enhanceSelects(root = document): для каждогоselect.fieldбез отметкиdata-enhancedстроит<details class="menu select-menu">+<summary class="field select-btn">(текст выбранного<option>+ шеврон, как у кнопок меню)<div class="menu-panel">с<button type="button" class="mi" data-value="…">на каждый<option>; выбранный —.mi.on(«✓» уже есть в CSS). Ширина берётся изstyle.widthселекта (у фильтров заданы 150–200px, в форме — 100%); панель не уже кнопки.aria-labelселекта (или текст связанной подписи) переносится наsummary;aria-haspopup="listbox". Селект получаетhiddenиdata-enhanced, егоtabindex= -1.
- Выбор пункта:
select.value = …,select.dispatchEvent(new Event("change", { bubbles: true }))— срабатывают inlineonchange,hx-trigger="change"формы#dev-filtersи обработчикdata-newgroup; обновить текст кнопки и.on; закрыть меню. - Синхронизация: класс
.on(включённый фильтр) — зеркалить с селекта наsummaryвsyncFiltersи при выборе (сейчас.onставится наselect.field— перенести подсветку на кнопку). - Вызов: при
initи вhtmx:afterSwapдля вставленного фрагмента (окно устройства, таблица с OOB-фильтрами). - Клавиатура:
summaryфокусируется Tab; Enter/Space открывают; в открытом меню ↑/↓ — между пунктами, Enter — выбрать, Escape — закрыть (существующий обработчик) и вернуть фокус на кнопку. - Переиспользовать существующее:
placeMenu(позиционирование fixed, раскрытие вверх, не обрезается таблицей/окном),closeMenus, закрытие по клику вне и после выбора пункта,reflowMenu. Новый код позиционирования не писать. - Автообновление таблицы (
autoRefresh) уже не срабатывает при открытомdetails— поведение сохраняется.
app/ui/static/style.css
.select-btn: вид как уselect.field(высота 36,padding: 0 36px 0 14px,font-weight: 500, шеврон справа,cursor: pointer, обрезка длинного текстаtext-overflow: ellipsis),list-style: noneи скрытие маркераdetails;.select-btn.on— какselect.field.on..select-menu .menu-panel:max-heightс прокруткой (длинные списки устройств/типов событий),min-width= ширина кнопки.- Только существующие токены (
--surf,--line,--acc50,--accd,--shadow-menu…); литеральных цветов нет, тёмная тема — через токены.
Шаблоны
- По возможности без изменений. Исключение —
_device_form.html:<select>внутри<label>; убедиться, что клик по кнопке списка внутри<label>не вызывает побочного действия подписи (двойное переключениеdetails). Если вызывает — заменить<label class="lbl">у этого поля на<div class="lbl">с подписью<span id>иaria-labelledbyу кнопки.
Тесты (минимально)
Логика на клиенте, автотестов JS в проекте нет. Серверные тесты не меняются; проверка — вручную (раздел «Проверка»).
Документация
README, раздел «Интерфейс»: выпадающие списки в стиле меню действий (без JavaScript — стандартные). Строка 020 в «История изменений».
summary.md — оркестратор.
Исполнение
Исполнитель (Sonnet): app.js, style.css, при необходимости _device_form.html, README; пересборка стенда. Тесты не запускает, не коммитит.
Изменение 019 в рабочем дереве не закоммичено — файлы 019 не трогать (кроме README: строка 020 добавляется после строки 019).
Проверка
- Стенд (override 8001) отдаёт новый
app.js(curl -s localhost:8001/static/app.js | grep -c enhanceSelects≥ 1). pytest— без регрессий.- Ручная проверка UI — пользователь (светлая и тёмная тема, Ctrl+F5):
- «Устройства»: фильтры Статус/Обновления/Канал — выбор применяет фильтр, «Сбросить (N)» считает, подсветка включённого фильтра;
- «Бэкапы», «Журнал»: выбор перезагружает страницу с фильтром, выбранный пункт отмечен «✓»;
- окно устройства: «Группа» → «+ Новая группа…» показывает поле названия; сохранение привязывает группу;
- список открывается над таблицей/окном без обрезки, вверх у нижнего края; клавиатура: Tab, Enter, ↑/↓, Escape.