sdlc-hub/docs/superpowers/plans/2026-10-21-redesign.md
Andrey Epifancev aad35177bf План: редизайн веб-приложения (#65)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 23:57:47 +04:00

50 KiB
Raw Blame History

Редизайн веб-приложения по дизайн-системе AURA — Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: веб-приложение хаба выглядит как AURA (светлая и тёмная темы), собрано из общих компонентов и токенов; «Обновить из Jira», тема и «Сменить токен» — в верхней панели на всех страницах; фильтры доски и списка сгруппированы.

Architecture: слой токенов web/src/styles/tokens.css (AURA + токены доски) и набор компонентов web/src/ui/ (вид и доступность, без логики страниц). Обновление из Jira переезжает из board/useBoardSource.ts в оболочку (shell/refresh.ts: хук, контекст, провайдер), доска оставляет себе источник и события снимка. Страницы перекладываются на компоненты по одной; логика доски (board/{model,analysis,layout}, storage.ts) и golden-эталоны не меняются.

Tech Stack: React 19 + TypeScript + Vite, TanStack Query, Vitest + Testing Library, Playwright; @fontsource/mulish 5.3.0, @fontsource/ibm-plex-mono 5.3.0.

Spec: openspec/changes/redesign/ (proposal.md, design.md — решения 1–24 и значения токенов в «Context», specs/web-app/spec.md). Материалы системы (вне репозитория): ~/projects/jira-bridge/design/ — экспорт Figma *.svg (смотреть как картинки), AURA-xlsx-tool-handoff-2026-09-29.zip (src/styles/tokens.css, src/styles.css, design-system/tokens.json — значения для сверки).

Global Constraints

  • Цвета светлой и тёмной темы — ровно значения из design.md «Context» (канва #f8f8f6 / #181818, поверхность #fdfdfc / #1f1f1f, текст #17171a / #f2f4f2, вторичный #818190 / #d6dcd7, граница #e5e5e0 / rgb(232 238 233 / 10%), действие #568d32 / #69a93d, hover #477729 / #83c657, ошибка #b42318 / #e26c67, предупреждение #d89614 / #e8c17a, фокус rgb(42 122 70 / 35%) / rgb(116 184 255 / 55%)).
  • Вне web/src/styles/tokens.css в *.css нет литералов цвета (#hex, rgb(, rgba(, hsl(, именованных цветов); разрешены transparent, currentColor, inherit, color-mix(...) от переменных.
  • Типографика: Mulish; текст 14/20 Regular, подпись 14/20 Semibold (600), мелкий 12/16, крупный 16/24, заголовок страницы 20/28 Bold (700). Моно — IBM Plex Mono (ключи, код, исходник).
  • Геометрия: шаг 4 px; контрол 32 px (крупный 40), радиус контрола 10; кнопка — радиус 999; панель — радиус 16; окно — радиус 20; флажок 16×16 радиус 4; переходы 180 ms ease; фокус — кольцо 2 px --color-focus-ring со смещением 2 px.
  • Контейнер страниц, кроме доски: max-width: 1440px, по центру, боковой отступ 24 px (16 px уже 640 px). Доска — вся ширина, отступ 24 px.
  • Адреса, query-параметры, ключи localStorage (graph-*, jira-hub-token, jira-hub-project…), запросы к API, тексты сообщений statusText — прежние; новые ключи только jira-hub-legend (localStorage) и jira-hub-issue-filters (sessionStorage).
  • Golden-эталоны web/src/board/__fixtures__/*.expected.json и модули board/{model,analysis,layout} не менять.
  • Новых зависимостей, кроме @fontsource/mulish@5.3.0 (точная версия); @fontsource/ibm-plex-sans удалить.
  • dangerouslySetInnerHTML/innerHTML не появляются; иконки — React-SVG.
  • Подписи и комментарии — русские; коммиты заканчиваются строкой Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>.
  • Зелёным считается только весь набор: make check и cd web && npm run e2e.

Review Focus

  1. Пакет обновления, начатый на доске, при уходе на «Задачи» и обратно: один опрос, кнопка остаётся недоступной, по завершении список и доска перечитаны (тест в Task 4 «переход между страницами во время пакета»).
  2. Доска в режиме «из файла» в момент завершения пакета: файл остаётся (тест в Task 4, перенос M-1).
  3. Тёмная тема в окне карточки, меню и выпадающих списках: родной <dialog> и <select> без color-scheme рисуются светлыми (тест в Task 1: color-scheme: dark в тёмных блоках tokens.css; скриншоты Task 11).
  4. Смена шрифта и сохранённые размеры карточек: текст не вылезает за карточку, высота по содержимому считается тем же слоем .measure (e2e «растяжка за уголок и возврат размера» в Task 7 остаётся зелёным).
  5. Список с выбранными фильтрами, открытый по адресу в новой вкладке: панель «Фильтры» свёрнута, но плашки и счётчик видны (тест в Task 8 «Фильтр в адресе»).

Task 1: Токены, шрифт, проверка цветов

Files:

  • Create: web/src/styles/tokens.css, web/src/styles/tokens.test.ts
  • Modify: web/src/styles/theme.css (удалить файл; импорт заменить на tokens.css в web/src/App.tsx), web/src/styles/base.css, web/src/styles/controls.css, web/src/board/board.css, web/src/main.tsx, web/package.json, web/package-lock.json

Interfaces:

  • Produces: CSS-переменные для всех задач. Семантические (обе темы): --color-bg-canvas, --color-bg-surface, --color-bg-subtle, --color-bg-selected (#f4faf5 / #272727 — фон выбранного), --color-bg-hover (#f2f2f0 / #272727), --color-bg-track (трек сегмента, #eeeeeb / #272727), --color-text-primary, --color-text-secondary, --color-text-placeholder, --color-text-disabled, --color-text-on-primary, --color-border-default, --color-border-strong, --color-border-selected, --color-action-primary, --color-action-primary-hover, --color-action-danger, --color-action-danger-hover, --color-status-success, --color-status-success-bg, --color-status-error, --color-status-error-bg, --color-status-warning, --color-status-warning-bg, --color-status-warning-border, --color-focus-ring, --color-overlay (rgb(0 0 0 / 45%)), --shadow-popover, --shadow-dialog. Шкалы: --space-1…8 (4…32), --radius-xs/sm/md/lg/xl/2xl/round (4/8/10/12/16/20/999), --font-ui ("Mulish", system-ui, -apple-system, "Segoe UI", sans-serif), --font-mono, --fs-caption 12, --fs-body 14, --fs-lg 16, --fs-section 20, --lh-caption 16, --lh-body 20, --lh-lg 24, --lh-section 28, --control-h 32, --control-h-lg 40, --control-radius 10, --motion 180ms ease. Токены доски: --k-decision, --k-analytics, --k-dev, --k-ui, --k-test, --k-org, --p-crit, --p-high, --p-mid, --p-low, --edge, --edge-strong (= действие), --guide (оранжевый, прежний), --lane-alt. Псевдонимы на переход (удаляются в Task 10): --bg→canvas, --fg→text-primary, --muted→text-secondary, --line→border-default, --card→surface, --panel→surface, --soft→bg-hover, --accent→action-primary, --accent-fg→text-on-primary.

  • Тёмная тема — те же три блока, что сейчас в theme.css: @media (prefers-color-scheme: dark){ :root:not([data-theme="light"]){…} } и :root[data-theme="dark"]{…}; в обоих color-scheme: dark, в светлом :root — color-scheme: light.

  • Step 1: Тест tokens.test.ts (Vitest, node fs): (а) «вне tokens.css нет литералов цвета» — обходит все web/src/**/*.css, кроме styles/tokens.css, ищет /#[0-9a-fA-F]{3,8}\b|\b(rgb|rgba|hsl|hsla)\(|:\s*(white|black|red|…)\b/ (именованные — список CSS-цветов из 10–15 самых частых достаточно: white, black, red, green, blue, gray, grey, orange, yellow, purple) вне комментариев; падение перечисляет файл: литерал. (б) «обе темы задают одни имена» — множество --color-* в светлом :root равно множеству в :root[data-theme="dark"] и в медиа-блоке. (в) тёмные блоки содержат color-scheme: dark.

  • Step 2: cd web && npx vitest run src/styles/tokens.test.ts — FAIL (нет файла, литералы в controls.css, board.css).

  • Step 3: написать tokens.css по Interfaces и design.md «Context»; палитру --k-* подобрать под тёплую канву (светлая — насыщенность и светлота как у нынешних, различимы с #568d32; тёмная — светлее); перенести литералы controls.css (#fff, rgba(0,0,0,.18)) и board.css (rgba(0,0,0,.18)) в токены (--color-text-on-primary, --shadow-popover); удалить theme.css, импорт ./styles/tokens.css в App.tsx первым.

  • Step 4: шрифт: npm install --save-exact @fontsource/mulish@5.3.0, npm uninstall @fontsource/ibm-plex-sans; в main.tsx — @fontsource/mulish/400.css, 600.css, 700.css, @fontsource/ibm-plex-mono/500.css; base.css: body{font:var(--fs-body)/var(--lh-body) var(--font-ui);background:var(--color-bg-canvas);color:var(--color-text-primary)}, h1{font-size:var(--fs-section);line-height:var(--lh-section);font-weight:700}, :focus-visible — кольцо по Global Constraints, @media (prefers-reduced-motion: reduce){*{transition:none!important;animation:none!important}}.

  • Step 5: npx vitest run — весь набор зелёный (старые тесты не трогали разметку); npm run lint — чисто.

  • Step 6: Commit feat(web): токены AURA, шрифт Mulish, проверка цветов (#65)

Task 2: Иконки и базовые компоненты

Files:

  • Create: web/src/ui/icons.tsx, web/src/ui/Button.tsx, web/src/ui/Segmented.tsx, web/src/ui/ThemeSwitch.tsx, web/src/ui/Badge.tsx, web/src/ui/Notice.tsx, web/src/ui/Panel.tsx, web/src/ui/PageHeader.tsx, web/src/ui/ui.css, web/src/ui/index.ts, web/src/ui/ui.test.tsx
  • Modify: web/src/shell/theme.ts, web/src/shell/theme.test.ts

Interfaces:

  • Consumes: токены Task 1.

  • Produces (все из web/src/ui/index.ts):

    • Icon — компоненты IconRefresh, IconUpload, IconMore, IconClose, IconChevronDown, IconSearch, IconFilter, IconSun, IconMoon, IconExternal, IconWarning, IconCheck, IconError, IconInfo, IconHandoff, IconCritical, IconCalendar, IconNeighbors, IconLink, IconGrid, IconReset, IconLegend, IconOpen — (p: {size?: 16|20}) => JSX, <svg aria-hidden="true" width height viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">.
    • Button(p: {variant?: "primary"|"secondary"|"danger"|"ghost"; size?: "md"|"lg"; icon?: ReactNode; loading?: boolean} & ButtonHTMLAttributes) — класс ui-btn ui-btn--<variant> ui-btn--<size>, type="button" по умолчанию; loading — крутилка вместо icon, disabled, aria-busy="true". aria-pressed пробрасывается: при true у secondary — вид «выбрано» (фон --color-bg-selected, граница и текст --color-action-primary).
    • IconButton(p: {label: string; icon: ReactNode} & ButtonHTMLAttributes) — 32×32, радиус 10, aria-label={label}, title={label}.
    • Segmented<T extends string>(p: {label: string; value: T; options: {value: T; label: string; href?: string}[]; onChange?(v: T): void}) — role="radiogroup" + role="radio"/aria-checked для кнопок; если у вариантов есть href — <nav aria-label> со ссылками react-router Link и aria-current="page" у активной. Трек --color-bg-track, радиус 999, отступ 4; активный — поверхность, текст --color-action-primary, Semibold.
    • ThemeSwitch() — role="switch", aria-checked = тёмная, aria-label="Тёмная тема"; читает/пишет через theme.ts.
    • Badge(p: {tone?: "neutral"|"success"|"error"|"warning"|"info"; children}) — 20 px высота, радиус 999, 12/16 Semibold.
    • Chip(p: {children; onRemove(): void; removeLabel: string}) — плашка с кнопкой ✕ (aria-label={removeLabel}), вид «выбрано».
    • Notice(p: {tone: "success"|"error"|"warning"|"info"; title?: ReactNode; children?; action?: ReactNode}) — role="alert" для error/warning, role="status" для прочих; иконка в круге 20 px, заголовок 14/20 Semibold, текст 12/16, радиус 12, граница/фон тона.
    • Panel(p: {as?: "div"|"section"; padding?: "none"|"md"|"lg"} & HTMLAttributes) — поверхность, граница, радиус 16, отступ 16/24.
    • PageHeader(p: {title: ReactNode; subtitle?: ReactNode; actions?: ReactNode; children?: ReactNode}) — <h1>, подзаголовок 14/20 вторичным, действия справа (перенос на узких).
  • theme.ts: THEMES, readTheme, saveTheme, applyTheme прежние; новые systemDark(): boolean (matchMedia("(prefers-color-scheme: dark)"), без matchMedia — false) и effectiveDark(t: Theme): boolean (t === "dark" || (t === "" && systemDark())). THEME_NAMES удалить, если не используется.

  • Step 1: Тесты ui.test.tsx: Button loading — aria-busy="true", недоступна, иконка не показана; Button aria-pressed пробрасывается; IconButton — доступное имя из label; Segmented кнопками — radiogroup, щелчок вызывает onChange("b"), у выбранного aria-checked="true"; Segmented ссылками — внутри MemoryRouter, активная ссылка aria-current="page"; ThemeSwitch — без graph-theme и с matchMedia → dark показывает aria-checked="true", щелчок → graph-theme = light, data-theme="light"; второй щелчок → dark; Chip — кнопка «Снять фильтр Спринт» вызывает onRemove; Notice tone="error" — role="alert". theme.test.ts: effectiveDark("") следует matchMedia, effectiveDark("light") === false.

  • Step 2: npx vitest run src/ui src/shell/theme.test.ts — FAIL.

  • Step 3: реализовать компоненты и ui.css (только переменные из Task 1; размеры — Global Constraints; Button primary — фон действия, текст --color-text-on-primary, hover — --color-action-primary-hover; secondary — прозрачный фон, граница --color-border-strong, текст основной, hover — граница действия; danger — фон --color-action-danger; ghost — без рамки, текст действия; lg — 40 px, отступ 20).

  • Step 4: npx vitest run и npm run lint — зелёные.

  • Step 5: Commit feat(web): иконки и базовые компоненты дизайн-системы (#65)

Task 3: Компоненты форм, окно, меню

Files:

  • Create: web/src/ui/Field.tsx, web/src/ui/inputs.tsx (TextInput, SearchInput, Select, Checkbox, Textarea), web/src/ui/Dialog.tsx, web/src/ui/Menu.tsx, web/src/ui/forms.test.tsx
  • Modify: web/src/ui/index.ts, web/src/ui/ui.css, web/src/board/components/Dropdown.tsx (переносится в ui/Menu.tsx как Dropdown; старый файл — реэкспорт или удаление с правкой импортов MultiSelect, Toolbar)

Interfaces:

  • Produces:

    • Field(p: {label: string; hint?: ReactNode; error?: ReactNode; children: ReactElement}) — <label> с подписью 14/20 Semibold над контролом, подсказка 12/16 вторичным, ошибка — цветом ошибки с role="alert".
    • TextInput, Textarea — обёртки над <input>/<textarea> с классом ui-input (32 px, радиус 10, отступ 12, граница --color-border-default, hover — --color-border-strong, фокус — граница действия + кольцо, aria-invalid="true" — граница ошибки); size?: "md"|"lg".
    • SearchInput(p: {value: string; onChange(v: string): void; label: string; placeholder?: string}) — type="search", aria-label={label}, иконка поиска слева, кнопка «Очистить» (IconClose, видна при непустом значении).
    • Select(p: SelectHTMLAttributes & {size?: "md"|"lg"}) — родной <select> с appearance:none и IconChevronDown поверх справа (pointer-events:none).
    • Checkbox(p: {label: ReactNode} & InputHTMLAttributes) — <label> + <input type="checkbox"> 16×16, радиус 4, accent-color действия.
    • Dialog(p: {label: string; onClose(): void; size?: "sm"|"md"|"lg"; className?: string; children}) — родной <dialog>, showModal() при монтировании, aria-label, onCancel → preventDefault + onClose; ширина sm 480 / md 640 / lg 1000 px (min(…, 100vw - 32px)), радиус 20, фон поверхности, ::backdrop — --color-overlay; уже 700 px — во весь экран, как сейчас у .issue-modal. Закрытие по фону — ответственность использующего (IssueModal уже это умеет).
    • Dropdown — прежняя сигнатура ({button, className, children, onOpen}), классы dd, dd-menu, menu сохраняются; вид — поверхность, граница, радиус 12, --shadow-popover, пункты 32 px радиус 8.
    • Menu(p: {label: string; children: (close: () => void) => ReactNode; align?: "start"|"end"}) — Dropdown с IconButton label={label} icon={<IconMore/>}; пункты — MenuItem(p: {onSelect(): void; pressed?: boolean; danger?: boolean; icon?; children}) (role не меняем: кнопки, aria-pressed для переключателей), MenuSeparator().
  • Step 1: Тесты forms.test.tsx: Field связывает подпись с полем (getByLabelText("Метка")); SearchInput — ввод вызывает onChange, «Очистить» → onChange("") и видна только при значении; Select — getByRole("combobox", {name}) и выбор значения; Dialog — открыт (open), Esc вызывает onClose; Menu — кнопка «Ещё» открывает пункты, MenuItem pressed → aria-pressed="true", выбор пункта вызывает onSelect и закрывает меню; Escape и щелчок вне закрывают (как в нынешнем Dropdown.test.tsx — перенести его в ui/).

  • Step 2: npx vitest run src/ui — FAIL.

  • Step 3: реализовать; импорты Dropdown в board/components перевести на ../../ui.

  • Step 4: npx vitest run и npm run lint — зелёные.

  • Step 5: Commit feat(web): поля, выпадающие списки, окно и меню дизайн-системы (#65)

Task 4: Обновление из Jira — в оболочку

Files:

  • Create: web/src/shell/refresh.ts (хук и контекст), web/src/shell/RefreshProvider.tsx, web/src/shell/refreshStatus.ts, web/src/shell/refresh.test.tsx
  • Modify: web/src/board/useBoardSource.ts, web/src/board/status.ts, web/src/board/useBoardSource.test.tsx, web/src/App.tsx (провайдер внутри QueryClientProvider и AuthGate)

Interfaces:

  • Produces:

    • refreshStatus.ts: тип RefreshEvent = прежние пакетные виды BoardStatus (batch, batch-done, batch-failed, batch-cancelled, batch-lost) + {kind: "hub-down"} + {kind: "error"; message: string}; refreshText(e: RefreshEvent): {text: string; tone: "info"|"success"|"error"|"warning"} — тексты пакетных видов дословно из нынешнего statusText; hub-down — «хаб не отвечает — пакет не поставлен».
    • refresh.ts: interface JiraRefresh { event: RefreshEvent | null; busy: boolean; agent: AgentView | null; start(): Promise<void>; track(batchId: number): void }, RefreshContext, useJiraRefresh(): JiraRefresh (вне провайдера — ошибка с понятным текстом).
    • RefreshProvider({children, pollMs = POLL_MS}) — владелец состояния: постановка (GET /agent → POST /refresh {full: true, author: "viewer"}), один опрос ["batch", id] (правила остановки — как сейчас в useBoardSource: терминальный статус, AuthError/ApiError, MAX_POLL_ERRORS подряд), подхват незавершённого пакета viewer с первой командой refresh при монтировании и после каждой успешной загрузки запроса с ключом ["board", …] (подписка queryClient.getQueryCache().subscribe, событие updated с action.type === "success"), кроме перечитывания после своего пакета; GET /agent — useQuery(["agent"], refetchInterval 60_000). Завершение: invalidateQueries для ["board"], ["issues"], ["issue"], ["projects"]; событие batch-done сбрасывается в null через 10 с. track(id) — начать опрос известного пакета (форма «Новый проект»).
    • useBoardSource после правки: BoardSource без refreshing и refreshFromJira; status — только события доски (loading, hub-down, file-error, json-applied, error); BoardStatus в board/status.ts — без пакетных видов, statusText — без них.
  • Step 1: refresh.test.tsx — перенести из useBoardSource.test.tsx тесты M-2 (двойное нажатие — один пакет, один опрос), M-3 (подхват после загрузки снимка), M-4 (404, 5 ошибок подряд), T10.2 (4xx — batch-lost с detail, busy сброшен), «пакет с ошибкой команды; агент офлайн», «уход и возврат во время пакета», T10.6 (пакет без refresh не подхватывается), 401 на запуске (токен сброшен), запоздавший 401 — на RefreshProvider через renderHook(useJiraRefresh, {wrapper}). Новые: «по завершении инвалидируются board, issues, issue, projects» (шпион на invalidateQueries или счётчики запросов GET /issues и GET /board); «track(9) опрашивает пакет 9 без POST /refresh»; «batch-done исчезает через 10 с» (фальшивые таймеры); «переход между страницами во время пакета» — провайдер смонтирован один раз, хук доски размонтирован и смонтирован снова: POST /refresh один раз, maxInFlight("GET /batches/7") === 1, busy === true до завершения.

  • Step 2: в useBoardSource.test.tsx убрать перенесённые тесты; оставить M-1 (поздний ответ, «Перечитать»), hub-down при «Перечитать», file-error, импорт JSON, T10.4 (JSON-снимок без хаба); «M-1: пакет, завершившийся при показанном файле, не заменяет файл» переписать: провайдер + доска, пакет завершился — source === "file". «T10.4: пакет завершился при показанном CSV, хаб не ответил» остаётся тестом доски (провайдер + доска): событие доски hub-down с fromFile. ««Обновить из Jira» при показанном CSV: хаб недоступен» переезжает в refresh.test.tsx: событие оболочки hub-down, доска по-прежнему из файла.

  • Step 3: npx vitest run src/shell/refresh.test.tsx src/board/useBoardSource.test.tsx — FAIL.

  • Step 4: реализовать refresh.ts, RefreshProvider.tsx, refreshStatus.ts; вычистить пакетную часть из useBoardSource.ts и status.ts. Toolbar.tsx и BoardPage.tsx временно берут кнопку из useJiraRefresh() (кнопка «Обновить из Jira» пока на прежнем месте — уйдёт в Task 5), строка состояния доски — statusText доски или, если его нет, refreshText пакета.

  • Step 5: npx vitest run и npm run lint — зелёные (тесты BoardPage.test.tsx про «Обновить из Jira» оборачиваются в провайдер через общий wrapper).

  • Step 6: Commit refactor(web): обновление из Jira — состояние оболочки, доска хранит только источник (#65)

Task 5: Верхняя панель приложения

Files:

  • Create: web/src/shell/AppBar.tsx, web/src/shell/DataStatus.tsx, web/src/shell/AppBar.test.tsx
  • Modify: web/src/shell/Layout.tsx, web/src/shell/shell.css, web/src/projects/ProjectPicker.tsx, web/src/projects/NewProjectDialog.tsx (onCreated(p: Project, batchId: number)), web/src/board/components/Toolbar.tsx (убрать «Обновить из Jira» и пункт темы), web/src/App.test.tsx, web/src/projects/projects.test.tsx, web/src/board/BoardPage.test.tsx, web/e2e/hub.ts (login ждёт ссылку «Доска связей»), web/e2e/*.spec.ts (подписи разделов)

Interfaces:

  • Consumes: useJiraRefresh (Task 4), Segmented, ThemeSwitch, Button, Menu/MenuItem, Select, Badge, иконки (Tasks 2–3); useBoard(project) для last_sync.

  • Produces: AppBar({section, project}: {section: "/board"|"/issues"; project: string | null}); DataStatus({project}: {project: string | null}) — точка агента + подпись («агент на связи» / «агент офлайн», title — «последний выход на связь: ДД.ММ ЧЧ:ММ»), «данные на ДД.ММ ЧЧ:ММ» (fmtStamp(last_sync.finished_at)) / «полного обновления не было», Button primary icon={<IconRefresh/>} loading={busy} «Обновить из Jira», текст события refreshText цветом тона (role="status").

  • Разметка панели (design.md, решение 9): <header class="appbar"> высота 64, поверхность, нижняя граница, отступы 24; «Хаб Jira» Bold 16/24; ProjectPicker на Select (метка aria-label="Проект", варианты и «+ Новый проект…» — прежние); Segmented ссылками «Доска связей» / «Задачи» (сохраняет ?project=); растяжка; DataStatus; ThemeSwitch; Menu label="Ещё" с MenuItem «Сменить токен». Уже 1100 px — DataStatus второй строкой (flex-wrap, order); уже 640 px — название скрыто, Segmented на всю ширину.

  • Step 1: Тесты AppBar.test.tsx: «Обновить из Jira» на /issues ставит POST /refresh и становится недоступной (aria-busy), после done — «пакет #7 выполнен» и GET /issues запрошен повторно; агент офлайн — «агент офлайн»; «данные на 14.10 12:00» из last_sync снимка; меню «Ещё» → «Сменить токен» показывает экран входа; разделы — ссылки «Доска связей» и «Задачи», активная с aria-current="page". projects.test.tsx: «Создание проекта из формы» — верхняя панель показывает «пакет #N в очереди» без GET /batches списка (через track). App.test.tsx/BoardPage.test.tsx: подписи «Доска» → «Доска связей», тест темы доски удалить (тема — в ui.test.tsx), тест «Обновить из Jira» доски переносится сюда.

  • Step 2: npx vitest run src/shell src/App.test.tsx src/projects — FAIL.

  • Step 3: реализовать AppBar, DataStatus; Layout рисует <AppBar> и <main className="page">; NewProjectDialog отдаёт batch_id из ответа, Layout вызывает refresh.track(batchId); убрать кнопку и тему из Toolbar.

  • Step 4: npx vitest run, npm run lint — зелёные; npm run e2e — зелёный.

  • Step 5: Commit feat(web): верхняя панель — разделы, данные Jira, обновление, тема (#65)

Task 6: Шапка и панель управления доски, легенда

Files:

  • Create: web/src/board/components/BoardHeader.tsx (вместо Toolbar.tsx, файл удалить), web/src/board/legendPref.ts
  • Modify: web/src/board/BoardPage.tsx, web/src/board/components/FilterBar.tsx, web/src/board/components/Legend.tsx, web/src/board/components/MultiSelect.tsx, web/src/board/components/ProblemsDrawer.tsx, web/src/board/components/ExportBox.tsx, web/src/board/components/LinkTable.tsx, web/src/board/board.css, web/src/board/BoardPage.test.tsx, web/e2e/board.spec.ts

Interfaces:

  • Consumes: PageHeader, Panel, Button, Badge, Menu/MenuItem/MenuSeparator, Notice, иконки.

  • Produces: BoardHeader(p) — пропсы нынешнего Toolbar без refreshing/onRefresh; readLegendOpen(): boolean, saveLegendOpen(open: boolean): void (localStorage["jira-hub-legend"], "0" — свёрнута, ошибки хранилища глотаются); Legend({prefs, open, onToggle}).

  • Разметка (design.md, решения 13–15): липкая шапка .sticky (сохранить headRef и --head-h) → PageHeader title="Доска связей" с подзаголовком-счётчиками (тексты счётчиков — нынешние .stats), источник — Badge «из файла: <имя>» + IconButton label="Вернуться к данным хаба" (= onReload), событие доски — statusText с тоном (role="status"); actions: Button secondary icon={<IconWarning/>} aria-pressed={problemsOpen} «Проблемы» + Badge tone={n ? "error" : "neutral"} со счётчиком, Menu label="Ещё": «Перечитать», «Загрузить файл…» (скрытый <input type=file> с прежним accept, пункт вызывает click()), «Экспорт JSON», «Таблица связей» (pressed), «Привязка к сетке» (pressed), MenuSeparator, «Сбросить раскладку» (danger, прежнее подтверждение внутри меню). Под шапкой — Panel .board-controls: MultiSelect спринтов и исполнителей (кнопка — вид Select, подпись прежняя), разделитель, группа «Показать» (подпись 12/16 вторичным + кнопки Button secondary aria-pressed «Соседи по связям» IconNeighbors, «Только передачи» IconHandoff, «Скрыть без связей» IconLink), группа «Режим» («Критический путь» IconCritical, «По датам» IconCalendar); title — нынешние. Легенда: Button ghost icon={<IconLegend/>} aria-expanded «Легенда» + Badge «скрыто: N» (N = hideKind.size + hideEdge.size, только при N > 0) и под ней пункты (прежняя логика). Ящик «Проблемы», экспорт, таблица связей — в Panel, заголовки 16/24 Semibold, ключи моно цветом действия.

  • Step 1: Тесты BoardPage.test.tsx: обновить подписи (кнопки режимов — «Соседи по связям», «Только передачи», «Скрыть без связей», «Критический путь», «По датам»; ключи graph-* в хранилище — прежние); меню «Ещё» → «Сбросить раскладку» с подтверждением; «Загрузить файл…» загружает CSV — Badge «из файла: jira-export.csv», кнопка «Вернуться к данным хаба» возвращает хаб; «Проблемы» — aria-pressed и счётчик; новый «Свёрнутая легенда» (спека): скрыт «тестирование», «Легенда» свёрнута → после перемонтирования свёрнута, «скрыто: 1», jira-hub-legend = "0"; на доске нет «Обновить из Jira» и пункта темы.

  • Step 2: npx vitest run src/board/BoardPage.test.tsx — FAIL.

  • Step 3: реализовать; e2e/board.spec.ts — "▥ по датам" → "По датам" и прочие подписи.

  • Step 4: npx vitest run, npm run lint, npm run e2e — зелёные.

  • Step 5: Commit feat(web): шапка доски, панель управления по группам, сворачиваемая легенда (#65)

Task 7: Полотно доски в стиле системы

Files:

  • Modify: web/src/board/board.css (разделы 6–9: полотно, дорожки, полоса дат, прокрутка, карточки, ящик), web/src/board/components/Card.tsx (значок «Открыть карточку» → IconOpen 16, aria-label прежний), web/src/board/components/Lane.tsx (кнопки дорожки — иконки, подписи прежние)

Interfaces:

  • Consumes: токены доски Task 1, иконки Task 2.

  • Вид (design.md, решение 16): .wrap — поверхность, граница, радиус 16; дорожки — чередование --lane-alt, подпись дорожки 12/16 Semibold вторичным (без text-transform); карточка — радиус 12, отступ 12, граница --color-border-default, hover — --color-border-strong; ключ — моно 13 px Semibold цветом действия; статус — вид Badge; .card.crit — кольцо --guide; ручки и уголок — цвет действия; полоса дат — поверхность, подписи 12/16 Semibold; .hscroll — бегунок --color-border-strong, hover — действие. Размеры карточек (CW, высоты, .measure) и классы, на которые опираются тесты (card, grip, rsz, dcol, dlabel, closed, out) — прежние.

  • Step 1: npx vitest run src/board — зелёный до правок (точка отсчёта).

  • Step 2: переписать стили и иконки.

  • Step 3: npx vitest run, npm run lint, npm run e2e (перетаскивание, растяжка, «по датам», приглушение закрытых) — зелёные; src/styles/tokens.test.ts — зелёный.

  • Step 4: Commit style(web): полотно доски — карточки, дорожки, даты в стиле системы (#65)

Task 8: Список задач

Files:

  • Create: web/src/issues/IssueFilters.tsx, web/src/issues/IssueTable.tsx
  • Modify: web/src/issues/IssuesPage.tsx, web/src/issues/issues.css, web/src/issues/IssuesPage.test.tsx, web/e2e/issues.spec.ts

Interfaces:

  • Consumes: PageHeader, Panel, SearchInput, Segmented, Button, Badge, Chip, Field, Select, Notice, IssueLink, isClosed, dueOf, withCurrent, filterOptions, epicNames, isOverdue/todayISO (issues/card.ts).

  • Produces: IssueFilters({filters, set, q, setQ, showClosed, opts, hasBoard}) — вся панель фильтров; IssueTable({rows, epicNames, closed, onOpen}), где onOpen(key) — тот же переход, что IssueLink (вынести из IssueLink.tsx хук useOpenIssue(): (key: string) => void, IssueLink пользуется им же).

  • Разметка (design.md, решения 17–18): PageHeader title="Задачи" subtitle="показано N" (+ « · есть ещё» при hasNextPage); Panel фильтров: SearchInput label="Поиск по ключу или теме" placeholder="Ключ или тема", Segmented label="Закрытые задачи" «Открытые» / «Все» (closed=1), Button secondary aria-pressed «Только соседи» (neighbor=1), Button secondary icon={<IconFilter/>} aria-expanded «Фильтры» + Badge с числом выбранных из шести; строка плашек: Chip «<подпись>: <значение>» (removeLabel="Снять фильтр <подпись>") для каждого выбранного, Button ghost «Сбросить»; раскрытая панель — сетка 3 колонки (2 уже 900 px, 1 уже 640 px) из Field + Select (aria-label прежний — подпись, варианты и «(нет на доске)» прежние). Раскрыта ли — sessionStorage["jira-hub-issue-filters"] ("1"), по умолчанию свёрнута. Таблица — Panel padding="none", шапка липкая, колонки в порядке спеки; ключ + Badge «сосед»; статус — Badge; срок просроченной незакрытой задачи — класс overdue (цвет ошибки) по isOverdue(due, status, closed, todayISO()); строка — onClick: если window.getSelection()?.toString() пусто и цель не внутри a, button → onOpen(key); cursor:pointer, hover — --color-bg-hover. «Показать ещё» — Button secondary по центру; пусто — Notice tone="info" «По этим фильтрам задач нет»; ошибка — Notice tone="error" с action «Повторить».

  • Step 1: Тесты IssuesPage.test.tsx: прежние сценарии на новых ролях («Открытые»/«Все» — radio, адрес closed=1, запрос с/без hide_closed=1; выбранный закрытый статус); «Фильтр в адресе» (спека): адрес со sprint= и assignee= → кнопка «Фильтры» со счётчиком 2, плашки «Спринт: AWPCA_Спринт 2» и «Исполнитель: …», панель свёрнута; «Снять фильтр плашкой» (спека); «Сбросить» снимает шесть списков, q и neighbor остаются; «Щелчок по строке» (спека) — щелчок по ячейке темы → адрес /issues/AURORA-…; выделенный текст в строке — без перехода; «просрочено» — класс overdue у срока незакрытой задачи с прошедшим сроком, у закрытой нет.

  • Step 2: npx vitest run src/issues/IssuesPage.test.tsx — FAIL.

  • Step 3: реализовать; e2e/issues.spec.ts — новые подписи.

  • Step 4: npx vitest run, npm run lint, npm run e2e — зелёные.

  • Step 5: Commit feat(web): список задач — поиск на виду, фильтры панелью с плашками, строка открывает карточку (#65)

Task 9: Карточка задачи и заметки

Files:

  • Modify: web/src/issues/IssueModal.tsx (на Dialog size="lg", поведение закрытия — прежнее), web/src/issues/IssueCard.tsx, web/src/issues/Notes.tsx, web/src/issues/NoteForm.tsx, web/src/issues/issues.css, web/src/issues/IssueCard.test.tsx, web/src/issues/IssueModal.test.tsx

Interfaces:

  • Consumes: Dialog, Button, IconButton, Badge, Panel, Field, Select, Textarea, Notice, иконки.

  • Вид (design.md, решение 19): шапка — ключ моно 13 px вторичным, Badge статуса, справа Button secondary icon={<IconExternal/>} «Открыть в Jira» (ссылка <a> с видом кнопки — класс ui-btn ui-btn--secondary) и IconButton label="Закрыть"; тема <h2> 20/28 Bold; факты 14/20 вторичным, «просрочено» — Badge tone="error"; заголовки разделов <h3> 16/24 Semibold без верхнего регистра; кнопки «исходник»/«текст», «развернуть», «Скрыть»/ «Показать» — Button ghost 32 px; правая колонка — Panel с фоном --color-bg-subtle; форма заметок — Field + Select/Textarea, «Сохранить» primary, «Отмена» secondary, «Править» secondary, ошибка — Notice error. Содержание, порядок, роли и тексты, на которые опираются тесты (data-testid, «исходник», «Скрыть», «Править», «Сохранить»), — прежние; «Открыть в Jira ↗» → «Открыть в Jira» (иконка вместо стрелки).

  • Step 1: обновить в тестах только изменившиеся подписи («Открыть в Jira»); npx vitest run src/issues — FAIL там, где подпись изменилась.

  • Step 2: переписать разметку и стили.

  • Step 3: npx vitest run, npm run lint, npm run e2e — зелёные.

  • Step 4: Commit style(web): карточка задачи и служебные заметки в стиле системы (#65)

Task 10: Вход, «Новый проект», служебные экраны, уборка старых стилей

Files:

  • Modify: web/src/auth/LoginScreen.tsx, web/src/projects/NewProjectDialog.tsx, web/src/projects/ProjectGate.tsx, web/src/projects/projects.css, web/src/shell/NotFound.tsx, web/src/shell/ErrorBoundary.tsx, web/src/styles/base.css, web/src/styles/controls.css (удалить, если всё перенесено в ui/ui.css), web/src/styles/tokens.css (удалить псевдонимы --bg, --fg, --muted, --line, --card, --panel, --soft, --accent, --accent-fg), все *.css, где они остались, web/src/auth/LoginScreen.test.tsx, web/src/projects/projects.test.tsx, web/src/App.test.tsx, ARCHITECTURE.md

Interfaces:

  • Вид (design.md, решение 20): вход — Panel 480 px по центру, <h2> «Вход в хаб» 20/28, пояснение, Field label="Клиентский токен хаба" + TextInput size="lg", Button primary size="lg" «Войти», ошибка — Notice error; «Новый проект» — Dialog size="sm" label="Новый проект", поля через Field, «Создать» primary, «Отмена» secondary, ошибка хаба — Notice error у кнопок; ProjectGate — Notice («проект X не найден» — warning с подсказкой; «Проектов нет» — info с Button primary «+ Новый проект…»; ошибка — error); «не найдено» и ошибка отрисовки — Notice в контейнере, ссылки «Доска связей», «Задачи» / Button «Перезагрузить». Классы .btn, .link, .dd-* из controls.css больше нигде не используются (поиск className="btn и "link" пуст).

  • ARCHITECTURE.md, абзац web/: токены web/src/styles/tokens.css, компоненты web/src/ui/, обновление из Jira — web/src/shell/refresh.ts (провайдер в App.tsx), доска — только источник и события снимка.

  • Step 1: обновить подписи в тестах (роли и тексты входа, «Новый проект», «не найдено» — прежние, кроме ссылок разделов); npx vitest run — FAIL там, где изменилось.

  • Step 2: переписать экраны; удалить псевдонимы и controls.css; заменить оставшиеся var(--bg) и прочие на семантические имена.

  • Step 3: grep -rn "var(--\(bg\|fg\|muted\|line\|card\|panel\|soft\|accent\|accent-fg\))" web/src — пусто; npx vitest run, npm run lint, npm run e2e, make check — зелёные.

  • Step 4: Commit style(web): вход, новый проект, служебные экраны; старые стили убраны (#65)

Task 11: Скриншоты и итоговая проверка

Files:

  • Create: web/e2e/screens.spec.ts
  • Modify: .gitignore (каталог web/e2e-screens/), web/playwright.config.ts (если нужен проект для SCREENS=1)

Interfaces:

  • screens.spec.ts: весь файл — test.skip(!process.env.SCREENS); для тем light и dark (через localStorage["graph-theme"] до загрузки) и вьюпортов 1920×1080 и 390×844 снимает fullPage в web/e2e-screens/<тема>-<ширина>-<экран>.png: login, board, board-bydate (режим «По датам»), board-problems (ящик открыт), issues, issues-filters (панель раскрыта, один фильтр выбран), card (/app/issues/AURORA-9001 поверх списка), new-project (окно открыто), refresh (пакет в очереди — без агента).

  • Step 1: написать спецификацию; cd web && SCREENS=1 npx playwright test e2e/screens.spec.ts — 36 файлов в web/e2e-screens/.

  • Step 2: просмотреть снимки (Read по файлам): нет наложений, обрезанных рамок и текста у границ, тёмная тема без светлых пятен (окно, меню, <select>), узкий экран без горизонтальной прокрутки страницы (кроме полотна доски); найденное — исправить в стилях соответствующей задачи и переснять.

  • Step 3: make check и cd web && npm run e2e — зелёные; git status — снимков в индексе нет.

  • Step 4: Commit test(web): скрипт скриншотов экранов для проверки вида (#65)