diff --git a/docs/tasks/gn-table-header-slot.md b/docs/tasks/gn-table-header-slot.md new file mode 100644 index 0000000..9fb7332 --- /dev/null +++ b/docs/tasks/gn-table-header-slot.md @@ -0,0 +1,109 @@ +# Задача: слот заголовка и ручки колонок у `GnTable` + +**Статус:** не начата. +**Тип:** API компонента Vue-адаптера (слоты + ручки колонок). Vanilla-разметка не +затрагивается. +**Повод:** потребитель кита (gntodo) собирает на `GnTable` журнал действий, и шапке +понадобился заголовок-иконка вместо слова — сейчас это достижимо только передачей +готового узла в `label`, нигде не описанной. Там же понадобилось прятать колонки на +узком экране, и сделано это по номеру колонки. Запись ведётся по правилу платформы: +находка про общий ресурс живёт в доке ресурса, у потребителя остаётся только рабочий +обход и ссылка сюда. + +**Источник:** gntodo — `frontend/src/views/JournalView.vue`, страница `/journal` +(правки 0.94); ui-kit 1.0.0 (коммит `e28f382`). +**Автор находки:** ИИ-агент (сессия разработки gntodo). **Автор задачи:** владелец +gntodo («замени текст на иконку коммента», «попробуй лучше адаптировать эту табличку +на моб»). +**Дата:** 2026-10-10. **Задача в трекере:** не заведена. + +## Проблема + +`src/vue/components/GnTable.js` рендерит шапку так: + +```js +h("tr", { class: "table-row" }, props.columns.map(column => h("th", { scope: "col" }, column.label))) +``` + +То есть у шапки **нет ни одного слота** (объявлены только `cell-${column.key}` и +`empty`), а `column.label` кладётся как есть. Отсюда два следствия, оба пойманы на +живом приложении: + +1. **Заголовок-иконка достижим только через `label`.** В gntodo в `label` передан + узел: `h('i', { class: 'ph ph-chat-text', role: 'img', 'aria-label': … })` — слово + «Комментарий» было шире колонки, в которой стоит одна кнопка. Работает (и это + спасибо — слот не понадобился), но поведение нигде не описано: в `component-api.md` + у `GnTable` перечислены только слоты ячеек, и потребитель сначала ищет несуществующий + `header-*`. Ничего, кроме `label`, в шапку положить нельзя — ни сортировку, ни + фильтр по колонке, ни кнопку. +2. **Адаптив таблицы потребитель делает по номеру колонки.** В gntodo на телефоне + скрыты две колонки: + `:deep(.table-row > :nth-child(4))`, `:nth-child(5)` — потому что у `columns` + нет ни `class`, ни `align`, ни `hidden`, а таблице целиком `attrs.class` + передаётся (`table data-list`), но колонкам — нет. Стоит поменять порядок колонок + в массиве — и стиль молча скроет не ту колонку, ошибка не проявится ни сборкой, + ни типами. + +## Предложение + +Минимальный шаг — **слот заголовка**, по аналогии с ячейками: + +```js +h("th", { scope: "col", class: column.class }, slots[`header-${column.key}`]?.({ column }) || column.label) +``` + +Плюс `column.class` в тот же `th`/`td` (у `td` сейчас тоже пустой объект атрибутов) — +тогда потребитель прячет колонку по стабильному имени, а не по индексу, и та же ручка +годится для выравнивания и ширины через его собственный CSS. + +Если решите делать слот — объявлять его в JSDoc и в `component-api.md` обязательно: +именно отсутствие строчки в доке и заставило искать обход. + +Необязательные шаги на будущее (не обязательны к этой задаче): `column.align` +(`left`/`right`), `column.width`, и сортировка по клику на `th` — последнее уже +потребует событий и состояния сортировки в компоненте, а сейчас `GnTable` полностью +управляется пропами. + +## Границы задачи + +- **Разметка и классы не меняются**: `.table-wrapper`, `table.table.data-list`, + `thead.table-head`, `tr.table-row`, `td.is-empty`, `caption.table-caption` — на них + завязаны стили кита и потребители. +- Слоты `cell-${column.key}` и `empty` — контракт не трогаем (`cell-*` получает + `{ row, column, value }`; у нового `header-*` достаточно `{ column }`). +- `columns` без новых полей обязан работать как раньше: `column.class` не задан — + атрибута нет вовсе (не `class="undefined"`). +- Vanilla-разметка таблиц в демо — обычный ``, ей ручки колонок не нужны; + но строку «заголовок можно отдать узлом» в `docs/components/data-display.md` стоит + добавить, потому что этот файл читают и потребители vanilla-сборки. + +## Что нужно сдать (приёмка по правилам репозитория) + +1. `src/vue/components/GnTable.js` — слот `header-${key}` и `column.class` в `th`/`td`, + JSDoc с новыми слотами. +2. `docs/vue/component-api.md` — строки про слоты `GnTable` (сейчас там только + `cell-*` и `empty`) и про то, что `label` рендерится как есть. +3. `docs/components/data-display.md` — пример таблицы с иконкой в заголовке. +4. `docs/catalog.json` (`npm run gen:catalog`) — если в каталоге у `GnTable` есть + список слотов. +5. Юнит-тест контракта в `tests/unit/`: слот `header-*` перекрывает `label`; + `column.class` попадает и в `th`, и в `td`; без слота и класса разметка прежняя. +6. `npm run release:check`. + +## Как проверить глазами + +Таблица из трёх колонок, у средней — `:class="'col-comment'"`, заголовок передан +узлом-иконкой, слот `#header-comment` передан кнопкой: + +- в DOM у `th` видно `class="col-comment"` и тот же класс у `td` во всех строках; +- при ширине 767px и меньше потребительский CSS скрывает колонку по имени класса, и + шапка остаётся согласованной с телом (число `th` равно числу `td`); +- колонки без `class` и без слота рендерятся ровно как в 1.0.0 (сравнить скриншотом). + +## Почему именно так (контекст от потребителя) + +В gntodo обе правки уже живут и проверены живым прогоном на 390px: заголовок-иконка +через `label`, скрытие колонок по `:nth-child`. Как только кит отдаст `header-*` и +`column.class`, потребитель заменит оба обхода — и вёрстка колонок перестанет +зависеть от порядка массива `columns`. До этого момента обход остаётся, а ссылка на +эту запись — в комментарии рядом с ним. diff --git a/docs/vue/component-api.md b/docs/vue/component-api.md index 24100ff..eaecd95 100644 --- a/docs/vue/component-api.md +++ b/docs/vue/component-api.md @@ -211,6 +211,12 @@ Slots: `cell-${column.key}`, `empty`. +No header slot: the header cell is rendered as `h("th", { scope: "col" }, column.label)`, +so `label` is inserted as-is and may be a VNode (an icon or a button) — undocumented but +works. `columns` accepts only `key` and `label`: no `class`, `align` or `hidden`, so +consumers hide columns on narrow screens by index (`:nth-child`), which breaks silently +when the order changes. See `docs/tasks/gn-table-header-slot.md`. + ### `GnToolbar`, `GnSearchField`, `GnPagination` Use for table/list screens.