From 7c4ba777b69aa3671f1748740a14fb49d5125f46 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 12 May 2026 02:35:41 +0000 Subject: [PATCH 1/7] chore(Filter): add planning docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan, spec, and morning checklist for the upcoming Filter component. No code yet — implementation starts after review. https://claude.ai/code/session_012S8FTBQd7v2E5nG868nxLh --- docs/plans/filter-component-plan.md | 371 ++++++++++++ docs/plans/filter-component-spec.md | 769 +++++++++++++++++++++++++ docs/plans/filter-morning-checklist.md | 95 +++ 3 files changed, 1235 insertions(+) create mode 100644 docs/plans/filter-component-plan.md create mode 100644 docs/plans/filter-component-spec.md create mode 100644 docs/plans/filter-morning-checklist.md diff --git a/docs/plans/filter-component-plan.md b/docs/plans/filter-component-plan.md new file mode 100644 index 000000000..00218cd54 --- /dev/null +++ b/docs/plans/filter-component-plan.md @@ -0,0 +1,371 @@ +# План разработки компонента `Filter` для b24ui + +> Источник: [docs/plans/filter-component-spec.md](./filter-component-spec.md) (исходная спецификация) +> Ветка: `claude/dev-plan-checklist-RrfmF` + +План разбит на фазы. Каждая фаза — самостоятельный коммит (conventional commits), который можно проверить и зареверсить. Все артефакты (компонент, тема, типы, тесты, docs, playground) идут по существующему workflow из `AGENTS.md`. + +--- + +## Принципы реализации + +1. **View-only.** Никаких REST-вызовов внутри. Всё через `props`/`emits`. +2. **Композиция из существующих b24ui-примитивов.** Не пишем своих низкоуровневых компонентов, если есть готовые (`InputTags`, `Popover`, `Drawer`, `CommandPalette`, `NavigationMenu`, `DropdownMenu`, `Form`, `FormField`, `ScrollArea`, `Button`, `Calendar`, `InputDate`, `InputNumber`, `InputTime`, `Select`, `SelectMenu`, `Tooltip`, `Empty`, `Skeleton`). +3. **Один публичный компонент `Filter`** + набор внутренних подкомпонентов, не экспортируемых наружу (пока). Если потребитель захочет частей — экспортируем после того, как API устаканится. +4. **Адаптив:** `useDevice` → `Popover` на desktop, `Drawer` на mobile. +5. **DnD:** обёртка `` поверх `@vueuse/integrations/useSortable`. Если потом понадобится переключиться — меняем одно место. +6. **Темизация:** `tailwind-variants` по конвенции репозитория (`src/theme/filter.ts` + slots для подкомпонентов). +7. **Локализация:** все строки UI («Найти», «Сбросить», «Любая дата», операторы и т.д.) — через `props` с дефолтами на русском. Не хардкодим в шаблонах. + +--- + +## Фаза 0 — Подготовка + +**Коммит:** `chore(Filter): add planning docs` + +- [x] Спецификация и скриншот в `docs/plans/filter-component-spec.md` + `filter-component-screenshot.png`. +- [x] Этот план в `docs/plans/filter-component-plan.md`. +- [x] Утренний чек-лист в `docs/plans/filter-morning-checklist.md`. + +Эта фаза уже сделана текущим коммитом — это якорь, чтобы при проверке утром был ясный baseline. + +--- + +## Фаза 1 — Типы и публичный контракт + +**Коммит:** `feat(Filter): define public types` + +**Файлы:** +- `src/runtime/types/filter.ts` (новый): + - `FieldType`, `FilterOperator`, `FieldOption`, `FieldConfig` + - `FieldCondition` (discriminated union по `operator`) + - `FilterValue = Record` + - `DatePreset`, `DateValue` + - `Preset` + - `FilterBarTag` + - `FilterProps`, `FilterEmits`, `FilterSlots` +- `src/runtime/types/index.ts` — реэкспорт типов фильтра. +- Утилиты-type-guards (`isFilledOp`, `isEmptyOp`, `isRangeOp`, `isInOp`) — `src/runtime/utils/filter.ts`. Нужны для нарроуинга в Vue-шаблонах (см. открытый вопрос #2 в спеке). + +**Что проверить перед коммитом:** `pnpm run typecheck`. + +--- + +## Фаза 2 — Тема + +**Коммит:** `feat(Filter): add theme` + +**Файлы:** +- `src/theme/filter.ts` — slots под всю анатомию: + - `root`, `bar`, `panel`, `presets`, `presetsList`, `presetItem`, `presetItemActive`, `fieldsEditor`, `field`, `fieldLabel`, `fieldControl`, `fieldMenuTrigger`, `dragHandle`, `actions`, `submitButton`, `resetButton`, `addFieldButton`, `defaultsButton`. + - Variants: `size` (`sm`/`md`/`lg`), `orientation` (`horizontal`/`vertical`), `color` (`air-primary` по дефолту). +- Регистрация в `ThemeDefaults` в `src/runtime/composables/useComponentProps.ts`. +- Подключение в `src/module.ts` (если требуется по существующему паттерну — проверить аналоги). + +**Проверка:** `pnpm run dev:prepare && pnpm run typecheck`. + +--- + +## Фаза 3 — Скаффолд компонента + базовая структура + +**Коммит:** `feat(Filter): scaffold component` + +Используем CLI: + +``` +bitrix24-ui make component Filter +``` + +Затем правим/добавляем подкомпоненты руками (CLI делает только один файл): + +**Файлы:** +- `src/runtime/components/Filter.vue` — корень, оркестрирует state, проксирует props/emits, рендерит `FilterBar` + `Popover`/`Drawer` со `FilterPanel`. +- `src/runtime/components/FilterBar.vue` — внутренний, оборачивает `InputTags`, формирует `FilterBarTag[]` через `computed`. +- `src/runtime/components/FilterPanel.vue` — внутренний, две колонки (`FilterPresets` + `FilterFieldsEditor`). +- `src/runtime/components/FilterPresets.vue` — внутренний. +- `src/runtime/components/FilterFieldsEditor.vue` — внутренний. +- `src/runtime/components/FilterField.vue` — внутренний, один контрол. +- `src/runtime/components/FilterSortableList.vue` — внутренний wrapper над `useSortable`. + +**Решение об экспорте:** наружу из `module.ts` экспортируем только `Filter`. Остальное — внутреннее. Если у потребителя возникнет потребность собирать кастомный bar — для этого уже есть slot `bar` в `FilterProps`. + +--- + +## Фаза 4 — FilterBar (свёрнутый вид) + +**Коммит:** `feat(Filter): implement FilterBar` + +- Корень — `InputTags` с `addOnBlur={false}`, `addOnTab={false}`, без `delimiter`. +- `convertValue`/`displayValue` — храним `FilterBarTag` объекты в `v-model`, рендерим через слот `item-text` (имя у InputTags может отличаться — проверить в `InputTags.vue` и подогнать). +- `computed` `tagsToShow`: + - первый тег — `preset` (если активный пресет есть); + - следующие до `visibleTagsCount` — `condition`; + - последний — `counter`, если осталось скрытых. +- `removeTag` обработчик: + - `kind: 'preset'` → emit `update:activePresetId` с `null`; + - `kind: 'condition'` → удаляем из `FilterValue`, emit `update:modelValue`; + - `kind: 'counter'` → открыть `FilterPanel`. +- `trailing` slot — `Button × 3` (лупа, ×, шестерёнка). Шестерёнка открывает панель. +- Клик по фону (не по чипу/инпуту) — открыть панель. Делаем через `data-slot="root"` + `@click.self`. +- Дебаунс `searchQuery` через `useDebounceFn` (vueuse). Default 300ms — `searchDebounce` prop. +- Enter в инпуте → `@apply`. + +--- + +## Фаза 5 — FilterPanel + адаптив + +**Коммит:** `feat(Filter): adaptive panel via useDevice` + +- `useDevice` для выбора `Popover` (desktop) vs `Drawer` (mobile). +- Panel рендерит две колонки на desktop, табы или вертикальный стек на mobile (в `Drawer`). +- Фокус-трап и Esc-закрытие — встроены в `Popover`/`Drawer`, дополнительно ничего не делаем. +- `defineShortcuts`: Enter → apply (если фокус внутри панели), Esc → close. + +--- + +## Фаза 6 — FilterPresets + +**Коммит:** `feat(Filter): implement presets column` + +- `NavigationMenu` (`orientation="vertical"`) для списка пресетов. +- Активный пресет — visual highlight (через theme slot `presetItemActive`). +- Системные пресеты (`system: true`) — нельзя удалить и переименовать (скрываем пункты в `DropdownMenu`). +- Пин-иконка (`B24Icons` pin) у пресета с `pinned: true`. +- `DropdownMenu` на каждом пресете: переместить (drag — отдельная иконка-ручка), pin/unpin, переименовать, удалить. +- **Drag&drop** через `FilterSortableList`. +- Inline-сохранение (`Desktop`) — `Input` появляется в конце списка после клика `+ Сохранить фильтр`. Enter → emit `presetSave`, Esc → отменить. +- Modal-сохранение (`Mobile`) — `useOverlay` + минимальный `Modal` + `Form` с одним `Input`. +- Confirm удаления — `useOverlay` + `Modal`. +- **Keyboard альтернатива DnD:** в `DropdownMenu` пункты «↑ Вверх» / «↓ Вниз» — emit `presetUpdate` с новым `order`. Это WCAG-страховка. + +--- + +## Фаза 7 — FilterFieldsEditor + пикер полей + +**Коммит:** `feat(Filter): implement fields editor` + +- `ScrollArea` (виртуализация — только если активных полей > 50; см. перфоманс-раздел спеки). +- Каждое поле — `FormField` + `FilterField`. +- Drag-handle ☰ слева (icon-only `Button`) + tooltip «Потяните, чтобы отсортировать список полей». +- Крестик удаления справа (`Button`, `variant="ghost"`, icon-only) — только при наведении (CSS hover-state, тема). +- `DropdownMenu` `...`: + - «Заполнено» → emit обновления с `{ operator: 'filled' }`; + - «Не заполнено» → `{ operator: 'empty' }`; + - «Очистить значение» → удалить ключ из `FilterValue`; + - «↑ Вверх» / «↓ Вниз» — keyboard альтернатива DnD. +- Кнопка `Добавить поле` → `CommandPalette` в `Popover` (desktop) или `Drawer` (mobile). + - Fuzzy-поиск по `field.label`. + - Группировка по `field.group` (visible label — `field.groupLabel`). + - Уже добавленные поля — `disabled`. +- Кнопка `Вернуть поля по умолчанию` → emit `fieldsReset`. Семантика по спеке: сбрасывает только набор полей (`activeFields ← defaultFields`), значения и активный пресет не трогает. +- Кнопки `Найти` (primary) / `Сбросить` (secondary). + - `Найти` — emit `apply` с `{ values, query, presetId }`. + - `Сбросить` — emit `reset` (обнуляет `FilterValue`, не трогает `activeFields`). + +**Важно:** все `