50 KiB
Редизайн веб-приложения по дизайн-системе 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
- Пакет обновления, начатый на доске, при уходе на «Задачи» и обратно: один опрос, кнопка остаётся недоступной, по завершении список и доска перечитаны (тест в Task 4 «переход между страницами во время пакета»).
- Доска в режиме «из файла» в момент завершения пакета: файл остаётся (тест в Task 4, перенос M-1).
- Тёмная тема в окне карточки, меню и выпадающих списках: родной
<dialog>и<select>безcolor-schemeрисуются светлыми (тест в Task 1:color-scheme: darkв тёмных блокахtokens.css; скриншоты Task 11). - Смена шрифта и сохранённые размеры карточек: текст не вылезает за карточку, высота по содержимому считается тем же
слоем
.measure(e2e «растяжка за уголок и возврат размера» в Task 7 остаётся зелёным). - Список с выбранными фильтрами, открытый по адресу в новой вкладке: панель «Фильтры» свёрнута, но плашки и счётчик видны (тест в 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-caption12,--fs-body14,--fs-lg16,--fs-section20,--lh-caption16,--lh-body20,--lh-lg24,--lh-section28,--control-h32,--control-h-lg40,--control-radius10,--motion180ms 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, nodefs): (а) «вне 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-routerLinkи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(значок «Открыть карточку» →IconOpen16,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 ghost32 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): вход —
Panel480 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)