feat(tgu-ui): выбор полосы слотов мышью «от первого к последнему»

Замена авто-чипа «полоса ×N» на ручной жест. Полоса переопределена:
один КА + один крен (ε) + подряд по времени; виток в границу не входит.

- slotStrips.ts: groupStrips → { idByKey, membersOf }; группировка по КА →
  кластеры по крену (ROLL_EPSILON=0.5) → деление по каденции (CADENCE_GAP_FACTOR=1.5)
- slotSelection.ts: selectStripRange вместо toggleStripSelection
- TguSlotsTab.tsx: клик по строке = якорь + выбор; Shift+клик тянет полосу
  между якорем и строкой; выход за полосу запрещён + подсказка; рельс полосы якоря
- tgu-planning.css: убраны стили чипа/hover; user-select:none на строках
- тесты обновлены (125 зелёных), tsc/build чисто
This commit is contained in:
Дмитрий Соловьев
2026-06-17 10:13:11 +03:00
parent 8faaa03af3
commit b092e50dd8
7 changed files with 322 additions and 296 deletions
@@ -40,25 +40,30 @@ export function selectionAfterCheck(
}
/**
* Новое множество выбора после клика по чипу «полоса». Поведение зеркалит «выбрать видимые»,
* но в рамках одной полосы: если ВСЕ её видимые выбираемые члены уже выбраны — снимаем их,
* иначе добавляем все. Скрытые (отфильтрованные) и BUSY-члены сюда не передаются — выбираем
* только то, что видно и можно бронировать (доверие счётчику «Выбрано»). Вход не мутируется.
* Новое множество выбора после жеста «полоса от первого к последнему»: добавляет всех членов
* полосы между fromKey и toKey включительно (в порядке времени). Это ВЫДЕЛЕНИЕ, не toggle —
* существующий выбор сохраняется. Жест ограничен одной полосой: вызывающий передаёт members
* именно её (один КА, один крен, подряд по времени); граница полосы — гарантия «нельзя выделить
* не полосу». Если fromKey или toKey не из members — выбор не меняется (страховка). Вход не мутируется.
*
* @param current Текущее множество выбранных ключей.
* @param memberKeys Ключи видимых выбираемых членов полосы.
* @param members Ключи членов полосы в порядке времени.
* @param fromKey Ключ первого клика (якорь).
* @param toKey Ключ второго клика.
*/
export function toggleStripSelection(
export function selectStripRange(
current: ReadonlySet<string>,
memberKeys: readonly string[],
members: readonly string[],
fromKey: string,
toKey: string,
): Set<string> {
const next = new Set(current)
if (memberKeys.length === 0) return next
const allSelected = memberKeys.every((k) => current.has(k))
for (const k of memberKeys) {
if (allSelected) next.delete(k)
else next.add(k)
}
const from = members.indexOf(fromKey)
const to = members.indexOf(toKey)
if (from < 0 || to < 0) return next
const lo = Math.min(from, to)
const hi = Math.max(from, to)
for (let i = lo; i <= hi; i++) next.add(members[i]!)
return next
}