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 продукта поставило «заменить полосу вкладок
рейкой» на первое место.
Два из трёх обоснований этого ревью были фактически неверны, и они зафиксированы здесь, чтобы никто не обсуждал их заново:
- «Второй уровень открывается поверх содержимого.» Неверно. Это встроенная строка
под верхней строкой —
GroupedTabs.tsx:143, соседний<div>внутри той же обёртки-колонки. Ничто никогда ничего не перекрывало. - «Счётчик пропадает, когда группа закрыта.» Неверно. Закрытая группа
суммировала счётчики дочерних и показывала итог —
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проходят без редактирования. DescribeuseAutoCollapseSidebarвsidebar-state.test.tsудалён, потому что удалён hook. -
Весь набор тестов web зелёный: 142 файла, 1 540 тестов.
Отброшенные альтернативы
- Рейка 48px только из иконок, буквально повторяющая
CodeShell. Отклонено: 12 вкладкам понадобились бы 12 различных иконок, которых не существует, а три заголовка групп — механизм, делающий читаемыми одновременные группы, — не имеют никакой формы иконки. Результатом был бы ряд квадратов без меток, каждому нужна подсказка, что хуже полосы, которую он заменяет. - Сохранить
GroupedTabsи сделать его вертикальным. Отклонено: выжило бы разделение на верхний/второй уровень, а это разделение — ровно то, что удаляется. Оставшийся код — позиционный обработчик клавиатуры в DOM,somarContagens,ultimaFilhaPorGrupo— это весь механизм для скрытия дочерних. - Сохранить автоматическое сворачивание и оставить две рейки рядом. Отклонено: вручает пользователю постоянную двойную рейку, навязанную системой, на вкладке, где горизонтальное место важнее всего, и пользователь не может это отменить (кнопка сворачивания была неактивна именно пока автоматическое сворачивание было включено).
- Удалить и
Tabs, раз у него теперь нет вызывающего. Отклонено по доказательствам: это опубликованный примитив дизайн-системы, а изменение разметки одного экрана — не причина менять инвентарь DS.