frontend · intermediate · 3d
Палитра команд
Палитра команд ⌘K с нечётким ранжированием, асинхронными источниками действий и полным клавиатурным управлением (стрелки, Enter, Escape, области видимости) — интерактивный слой, который нужен каждому профессиональному инструменту.
Результат
Палитра, открывающаяся по ⌘K, нечётко фильтрующая зарегистрированные команды при вводе, поддерживающая вложенные области (например, вход в подменю 'Theme >') и полностью управляемая только с клавиатуры.
Этапы
0/3 · 0%- 01Реестр + управление с клавиатуры
Собери реестр команд с синхронными командами, открытием/закрытием через ⌘K и обработкой клавиш стрелок, Enter и Escape.
Критерии готовности- ⌘K открывает/закрывает палитру; стрелки двигают активный пункт, Enter запускает, Escape закрывает — всё без мыши.
- Фокус заперт внутри открытой палитры и возвращается на триггер при закрытии.
- 02Нечёткое ранжирование
Добавь нечёткую фильтрацию со scoring-функцией, ставящей точные префиксные совпадения выше рассеянных, с подсветкой совпавших символов.
Критерии готовности- При вводе точные префиксные совпадения ранжируются выше разбросанных подпоследовательностей, а совпавшие символы подсвечиваются.
- Пустой запрос показывает все команды; нет совпадений — явное пустое состояние.
- 03Асинхронные источники с дебаунсом
Поддержи асинхронные источники действий (например, search API) с дебаунсом, индикатором загрузки и корректной отменой устаревших запросов.
Критерии готовности- Асинхронный источник дебаунсится, показывает загрузку, а устаревший in-flight запрос отменяется при смене ввода.
- Результаты отменённого запроса никогда не перезаписывают результаты последнего запроса.
Стартер
- README.md
- src/palette.ts
- test/palette.test.ts
Распакуй, реализуй заглушки, затем гоняй тесты, пока не позеленеют: bun test
Рубрика
| Джуниор | Миддл | Сеньор | |
|---|---|---|---|
| Качество совпадения | Фильтрует элементы по вхождению подстроки; все совпадающие элементы выводятся без учёта позиции. | Реализует поиск по подпоследовательности (не подстроке) и ставит префиксные / смежные совпадения выше рассеянных, чтобы нужный элемент оказывался первым. | Функция оценки чистая и тестируется отдельно; ничьи разрешаются детерминированно; функция корректно обрабатывает границы Unicode, а её временная сложность указана и обоснована (O(n·m) на элемент, итого O(N·m log N)). |
| Корректность клавиатурной модели | Стрелки перемещают выделение, Enter запускает выделенный элемент; перенос отсутствует или ломается на границах. | Выделение правильно оборачивается в обоих направлениях, включая список из одного элемента или пустой; reduce — чистая функция без побочных эффектов. | Редьюсер — единственный источник истины для всего клавиатурного состояния; управление фокусом (ловушка при открытии, восстановление при закрытии) вынесено за пределы редьюсера, чтобы чистая логика тестировалась в Node без DOM; граничные случаи (пустой список, быстрые нажатия) покрыты юнит-тестами. |
| Производительность под нагрузкой | Перестраивает весь список при каждом нажатии клавиши синхронно; незаметно при коротких списках (< 50 элементов). | Асинхронные источники дебаунсятся; функция ранжирования мемоизируется или пропускается при неизменном запросе; устаревшие in-flight запросы отменяются. | Список из 10 000+ команд отображается без лагов: либо виртуализирован (в DOM только видимые строки), либо проиндексирован заранее (trie / инвертированный индекс строится один раз при регистрации, чтобы работа на каждое нажатие была сублинейной). Выбор обоснован измеренным компромиссом между памятью и CPU. |
Эталонный разбор (спойлер)
Подпоследовательность vs подстрока: нечёткое сопоставление проверяет, что каждый символ запроса встречается в элементе по порядку, но не обязательно подряд — 'fc' совпадает с 'fuzzyCheck', потому что 'f' стоит перед 'c'. Расстояние Левенштейна — другая мера (минимум вставок/удалений/замен), которая одинаково штрафует пропуски; поиск по подпоследовательности дешевле (O(n) на кандидата) и ощущается естественнее для поиска команд, поскольку вознаграждает совпадение инициалов слов в camelCase.
Ранжирование как чистая функция: логика оценки должна принимать (items, query) и возвращать отсортированный массив без побочных эффектов — без мутаций состояния, без обращений к DOM, без асинхронной работы. Эта граница делает её юнит-тестируемой в Node, упрощает мемоизацию (те же входы → тот же выход) и позволяет слою отрисовки оставаться тонким представлением, которое только отображает ранжированные результаты в узлы DOM.
Дебаунс и отмена для асинхронных источников: дебаунс откладывает вызов асинхронного источника до паузы пользователя в наборе текста (обычно 150–250 мс), избегая сетевого round trip на каждое нажатие. AbortController позволяет отменить in-flight fetch, когда следующее нажатие происходит до получения предыдущего результата, предотвращая перезапись быстрого ответа на поздний запрос медленным ответом на ранний.
Виртуализация большого списка команд: рендеринг тысяч узлов DOM сразу вызывает перерасчёты макета. Виртуализатор (например, отрисовка только 8–12 видимых строк плюс небольшой буфер overscan) сохраняет DOM постоянного размера вне зависимости от длины списка. Компромисс — сложность отслеживания позиции прокрутки; для списков менее ~200 элементов накладные расходы виртуализации превышают выгоду, поэтому измеряй перед добавлением.
Сделай по-сеньорски
- Добавь области видимости: команда может открывать новый контекст (например, 'Switch project >') со своим отфильтрованным списком, навигацией-хлебными крошками и клавишей Backspace для выхода из области.
- Сделай палитру доступной: правильные ARIA-роли combobox + listbox, объявления live region о количестве результатов и focus trap, восстанавливающий фокус при закрытии.