Files
ayurishchevandClaude Opus 5.5 6b5c521461 Выпадающие списки в стиле меню действий
Все выпадающие списки (фильтры «Устройства», «Бэкапы», «Журнал», поле
«Группа» в окне устройства) оформлены как меню «⋯» (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>
2026-09-28 12:18:32 +03:00

69 lines
7.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# План: 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 }))` — срабатывают
inline `onchange`, `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.