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

468 lines
50 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Редизайн веб-приложения по дизайн-системе 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)`