Перейти к основному содержимому

ADR 0126 — Страница проекта навигируется вертикальной рейкой, а вкладка Código перестаёт сама сворачивать sidebar

  • Статус: принят
  • Дата: 2026-08-30
  • Ссылки (без редактирования): ADR 0078 (рамка экрана и реестр вкладок — он перенёс отступы полосы вкладок в примитив Tabs, который этот ADR теперь выводит из страницы проекта), ADR 0074 (долг по контрасту, из-за которого бейдж счётчика сплошной, а не тонированный), ADR 0125 (PR 0 того же принятого усилия из 8 PR, в котором этот — PR 1).
  • Пересматривает: RN-201 — половина с автоматическим сворачиванием удаляется; половина сохранения brabo.project/brabo.tab остаётся нетронутой.

Контекст​

На странице проекта 12 вкладок в 3 группах плюс 3 отдельные вкладки (apps/web/src/routes/project-tabs.ts), отрисовываемые двухуровневой горизонтальной полосой (components/ui/GroupedTabs.tsx поверх примитива Tabs). Дизайн-ревью UI продукта поставило «заменить полосу вкладок рейкой» на первое место.

Два из трёх обоснований этого ревью были фактически неверны, и они зафиксированы здесь, чтобы никто не обсуждал их заново:

  1. «Второй уровень открывается поверх содержимого.» Неверно. Это встроенная строка под верхней строкой — GroupedTabs.tsx:143, соседний <div> внутри той же обёртки-колонки. Ничто никогда ничего не перекрывало.
  2. «Счётчик пропадает, когда группа закрыта.» Неверно. Закрытая группа суммировала счётчики дочерних и показывала итог — somarContagens, GroupedTabs.tsx:33, покрыто GroupedTabs.test.tsx:141.

Выживший аргумент — единственная причина этого изменения: 12 элементов в горизонтальной панели не масштабируются. Полоса была спроектирована под ~6 вкладок handoff; группировка купила место, спрятав 9 из 12 за кликом, что является сжатием, а не размещением. В узком окне верхняя строка уже прокручивалась горизонтально (Tabs.module.css, overflow-x: auto).

Третье утверждение ревью — что пять счётчиков нужно суммировать в итоги групп — отклоняется здесь, как было отклонено раньше: пять очередей (insights, PRs, одобрения, backlog, архитектура) остаются раздельными, потому что их суммирование скрывает, какая очередь просит внимания. Это действующее продуктовое решение, уже записанное в doc-комментарии ContagensDeAba и в ProjectPage.tsx.

Решение​

GroupedTabs заменён на apps/web/src/routes/ProjectRail.tsx — вертикальную рейку между заголовком проекта и панелью вкладки.

Три группы открыты одновременно​

Это то, что покупает изменение, и поэтому заголовок группы перестал быть выбираемой кнопкой и стал заголовком. Не остаётся состояния «открытая группа», а значит, не остаётся и памяти «последней посещённой дочерней по группе» (ultimaFilhaPorGrupo) — прямой ссылке на вкладку группы теперь не нужна никакая обвязка, чтобы её показать, потому что она никогда не была скрыта.

Заголовки групп переиспользуют существующие ключи i18n groups.*.label, а метки вкладок — tabs.*.label, обе в namespace nav. project-tabs.ts не меняется: он уже был единым источником структуры, и рейка потребляет тот же GRUPOS_DO_PROJETO, что потребляла полоса. (Его известная асимметрия — ключ реестра arquitetura, отображаемый на ключ i18n tabs.architecture.label, — остаётся ровно как есть.)

Геометрия заимствована у рейки CodeShell, а не у Shell​

routes/code/CodeShell.module.css (.rail/.railItem/.railItemAtivo) — единственная рейка в репозитории с нарисованным активным состоянием: полоса --surface-1 с разделителем справа, элемент 32px с --radius-sm, и активный элемент, тонированный color-mix(in srgb, var(--accent) 12%, transparent) поверх текста --accent. .trilha из sidebar (Shell.tsx:444-477) не могла быть образцом: у неё нет ни активного элемента, ни клавиатурной навигации, поэтому она не отвечает ни на один из двух вопросов, на которые должен отвечать этот компонент.

Одно расхождение, объявленное: ширина. Рейка CodeShell шириной 48px, потому что она только из иконок. Эта — 180px, потому что несёт метки вкладок и заголовки групп — а это весь механизм, благодаря которому три группы остаются открытыми. Всё остальное (поверхность полосы, разделитель, высота элемента, радиус, заливка активного, hover, кольцо :focus-visible) — та же трактовка.

Бейдж счётчика остаётся сплошным --accent/--on-accent, а не 18-процентной заливкой из handoff. Это расхождение намеренное и уже зафиксировано в design/SCREENS.md: тонированная пара попадает в долг по контрасту (3,88:1), а это текст в 10px.

Клавиатурный контракт перенесён, а не брошен​

GroupedTabs.onKeyDownDaLinha (GroupedTabs.tsx:49-71) обрабатывал ArrowRight/ArrowLeft/Home/End с переходом на начало, и два из семи его тест-кейсов это покрывали (GroupedTabs.test.tsx:102,124). Удаление протестированной доступности без замены — регрессия, а не рефакторинг, поэтому тот же контракт снова появляется в рейке с переставленной осью: ArrowDown/ArrowUp/Home/End, с переходом, выбирая по мере движения (автоматическая активация, та же, что раньше).

Реализация лучше той, что она заменяет. GroupedTabs сопоставлял по позиции, запрашивая [role="tab"] в DOM, потому что примитив Tabs не выставлял refs. Рейка владеет собственными кнопками, поэтому Map refs с ключом по ключу вкладки даёт сопоставление без запроса к документу. Стрелки проходят весь сплющенный список, пересекая границы групп — в рейке нет «внешней строки» и «внутренней строки», которые надо разделять.

ARIA: рейка — role="tablist" с aria-orientation="vertical"; обёртки групп и их заголовки — role="presentation", что сохраняет 12 вкладок как элементы, напрямую принадлежащие tablist, при визуальной группировке.

GroupedTabs удалён; Tabs остаётся​

У обоих было единственное использование — GroupedTabs только в ProjectPage, Tabs только в GroupedTabs, — но это не вещи одного рода, и решающее доказательство — ds-bundle/, дизайн-система, сгенерированная из этого приложения (.ds-build-meta.json: source: web@0.1.0, 66 компонентов, namespace BraboDS):

  • Tabs в ней есть, как components/primitivas/Tabs — три записи в _ds_sync.json (.jsx, .d.ts, .prompt.md) и story в .stories-map.json. Его удаление сжало бы дизайн-систему как побочный эффект изменения разметки одного экрана. Обобщённый примитив без текущего потребителя — это запас, а не мёртвый код. Он остаётся, с комментарием, объясняющим почему.
  • GroupedTabs в ней нет — ноль вхождений во всём ds-bundle/. Это была композиция, специфичная для страницы проекта, поверх примитива, с ровно одним вызывающим, и она уходит вместе с таблицей стилей и GroupedTabs.test.tsx.

Последствия​

RN-201 теряет половину с автоматическим сворачиванием​

Вкладка Código принудительно сворачивала sidebar Shell через AutoCollapseContext / useAutoCollapseSidebar (apps/web/src/lib/sidebar-state.ts), регистрируемый ProjectCodeTab.tsx — единственным вызывающим — и объединяемый через ИЛИ со сворачиванием Shell в Shell.tsx:369-371,412. Всё это удалено: контекст, hook, состояние autoColapsado, Provider вокруг <Outlet />, состояние disabled кнопки сворачивания, существовавшее только для защиты автоматического пути, и строка sidebar.collapseButton.autoCollapsed на обоих языках.

Причина структурная: при постоянно присутствующей рейке проекта сохранение автоматического сворачивания поставило бы рейку иконок .trilha из Shell сразу рядом с новой рейкой проекта — две соседние вертикальные рейки, постоянно, на самой тяжёлой вкладке продукта.

Стоимость реальна и была рассмотрена и принята до написания этого. Измерено в браузере, а не оценено: вкладка Código теперь рендерится с sidebar Shell развёрнутой (264px) + рейка проекта (180px) + собственная рейка CodeShell (48px) = 492px рамки до первого символа кода, против ~110px раньше (свёрнутая sidebar 62px + рейка кода 48px). Это потеря ширины редактора, и это заплаченная цена.

Что покупается: автоматическое сворачивание было системой, решающей за пользователя; ручное сворачивание — это пользователь, решающий сам. Убрать решение системы, уважая решение пользователя, — вот обмен. Ручное сворачивание продолжает работать и продолжает сохраняться в brabo.sidebar.collapsed.

Двойная рейка не исчезла — она перенесена в состояние, выбранное пользователем. Ручное сворачивание по-прежнему даёт рейку иконок Shell рядом с рейкой проекта. Разница в том, кто выбрал. Говорить обратное было бы притворством.

RN-201 переписана на месте, с сохранением номера и якоря {#rn-201} (якоря — это контракт). Половина сохранения brabo.project/brabo.tab остаётся нетронутой.

Навигация по вкладкам теперь существует в двух местах — и уже существовала​

Shell.tsx рендерит собственный список вкладок по проекту в sidebar (LinhaDeAba, RN-196), читая тот же ABAS_DO_PROJETO. С рейкой страница проекта показывает эти же 12 вкладок второй раз, на несколько сотен пикселей правее. Это существовало ранее, а не введено здесь — горизонтальная полоса дублировала список sidebar ровно так же, — но рейка делает дублирование визуально параллельным (два вертикальных списка одних и тех же 12 меток), тогда как раньше это была строка против колонки. Согласование этих двух — отдельное продуктовое решение, и здесь оно не принимается.

Тесты​

  • GroupedTabs.test.tsx (7 кейсов) удалён вместе с компонентом. Два клавиатурных кейса снова появляются как пять в рейке (ProjectRail.test.tsx, 17 кейсов: группы открыты вместе, вертикальная ориентация tablist и 12 принадлежащих вкладок, отметка активной, прямая ссылка, пять раздельных счётчиков без суммы по группе, вкладка без счётчика без бейджа, и ArrowDown/ArrowUp/Home/End с переходом в обе стороны).

  • Два кейса в apps/web/src/routes/project-tabs.test.tsx переписаны, а не сохранены, и это единственное место, где это изменение редактирует тест, который должен был выжить. Оба утверждали контракт умирающего компонента через ProjectPage, а не реестр:

    • 'a régua de TOPO mostra os grupos e as abas soltas … nunca as 12 chaves achatadas' утверждал, что ровно 6 элементов имели role="tab". Весь смысл рейки в том, чтобы он был у всех 12. Переписан, чтобы утверждать тот же объявленный порядок, сплющенный, плюс то, что три заголовка групп существуют и что ни один из них не является вкладкой.
    • 'o selo do GRUPO é a SOMA dos selos das filhas' утверждал суммированный бейдж группы. Закрытой группы для сводки не осталось, а суммирование — именно то, чего этот продукт намеренно не делает. Переписан, чтобы утверждать, что каждая очередь сохраняет собственный бейдж и что ни один заголовок группы не несёт числа.

    Все остальные кейсы этого файла, и четыре в ProjectPage.test.tsx, весь Shell.test.tsx и describe 'projeto e aba ativos' в sidebar-state.test.ts проходят без редактирования. Describe useAutoCollapseSidebar в sidebar-state.test.ts удалён, потому что удалён hook.

  • Весь набор тестов web зелёный: 142 файла, 1 540 тестов.

Отброшенные альтернативы​

  • Рейка 48px только из иконок, буквально повторяющая CodeShell. Отклонено: 12 вкладкам понадобились бы 12 различных иконок, которых не существует, а три заголовка групп — механизм, делающий читаемыми одновременные группы, — не имеют никакой формы иконки. Результатом был бы ряд квадратов без меток, каждому нужна подсказка, что хуже полосы, которую он заменяет.
  • Сохранить GroupedTabs и сделать его вертикальным. Отклонено: выжило бы разделение на верхний/второй уровень, а это разделение — ровно то, что удаляется. Оставшийся код — позиционный обработчик клавиатуры в DOM, somarContagens, ultimaFilhaPorGrupo — это весь механизм для скрытия дочерних.
  • Сохранить автоматическое сворачивание и оставить две рейки рядом. Отклонено: вручает пользователю постоянную двойную рейку, навязанную системой, на вкладке, где горизонтальное место важнее всего, и пользователь не может это отменить (кнопка сворачивания была неактивна именно пока автоматическое сворачивание было включено).
  • Удалить и Tabs, раз у него теперь нет вызывающего. Отклонено по доказательствам: это опубликованный примитив дизайн-системы, а изменение разметки одного экрана — не причина менять инвентарь DS.