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