From 2a0b617cbaceaf6a360e093ce46edf94e598e715 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 22 Oct 2026 01:04:18 +0400 Subject: [PATCH] =?UTF-8?q?style(web):=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE?= =?UTF-8?q?=D1=87=D0=BA=D0=B0=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D0=B8=20?= =?UTF-8?q?=D0=B8=20=D1=81=D0=BB=D1=83=D0=B6=D0=B5=D0=B1=D0=BD=D1=8B=D0=B5?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=BC=D0=B5=D1=82=D0=BA=D0=B8=20=D0=B2=20=D1=81?= =?UTF-8?q?=D1=82=D0=B8=D0=BB=D0=B5=20=D1=81=D0=B8=D1=81=D1=82=D0=B5=D0=BC?= =?UTF-8?q?=D1=8B=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- web/src/issues/IssueCard.test.tsx | 6 +- web/src/issues/IssueCard.tsx | 50 ++++++------ web/src/issues/IssueModal.tsx | 12 ++- web/src/issues/NoteForm.tsx | 33 ++++---- web/src/issues/Notes.tsx | 3 +- web/src/issues/issues.css | 125 ++++++++++++++++++------------ web/src/ui/Dialog.tsx | 16 ++-- 7 files changed, 144 insertions(+), 101 deletions(-) diff --git a/web/src/issues/IssueCard.test.tsx b/web/src/issues/IssueCard.test.tsx index f80c656..5718ab2 100644 --- a/web/src/issues/IssueCard.test.tsx +++ b/web/src/issues/IssueCard.test.tsx @@ -46,12 +46,12 @@ describe("карточка задачи", () => { expect(await screen.findByRole("heading", { level: 2, name: "Тема AURORA-9001" })).toBeInTheDocument(); const head = screen.getByTestId("issue-head"); expect(within(head).getByText("AURORA-9001")).toBeInTheDocument(); - expect(within(head).getByText("В работе", { selector: ".badge.status" })).toBeInTheDocument(); + expect(within(head).getByText("В работе", { selector: ".ui-badge" })).toBeInTheDocument(); expect(head.querySelector(".badge.fact")).toBeNull(); // статуса по факту в карточке нет (hide-closed) expect(within(head).queryByText(/факт:/)).toBeNull(); expect(within(head).getByTestId("issue-facts")).toHaveTextContent("Иванов Иван Иванович · срок 10.01 просрочено · AWPCA_Спринт 2 · эпик: Тестовый эпик"); - expect(within(head).getByText("просрочено")).toHaveClass("overdue"); - expect(screen.getByRole("link", { name: "Открыть в Jira ↗" })).toHaveAttribute("href", "https://sdlc.go.rshbank.ru/jira/browse/AURORA-9001"); + expect(within(head).getByText("просрочено")).toHaveClass("ui-badge--error"); + expect(screen.getByRole("link", { name: "Открыть в Jira" })).toHaveAttribute("href", "https://sdlc.go.rshbank.ru/jira/browse/AURORA-9001"); }); it("связи по группам с темой и статусом; задача вне зеркала; прочий тип с направлением", async () => { diff --git a/web/src/issues/IssueCard.tsx b/web/src/issues/IssueCard.tsx index 14757b7..40c8942 100644 --- a/web/src/issues/IssueCard.tsx +++ b/web/src/issues/IssueCard.tsx @@ -8,6 +8,7 @@ import { fmtDate, fmtStamp } from "../board/model/util.ts"; import { groupLinks, isOverdue, todayISO } from "./card.ts"; import { useCommentsHidden } from "./commentsPref.ts"; import { IssueLink } from "./IssueLink.tsx"; +import { Badge, Button, IconButton, IconClose, IconExternal, Notice, Panel } from "../ui/index.ts"; import { Notes } from "./Notes.tsx"; import { dueOf } from "./options.ts"; import { WikiText } from "./wiki/WikiText.tsx"; @@ -19,17 +20,23 @@ export function IssueCard({ issueKey: key, onClose }: { issueKey: string; onClos const issue = useIssue(key); const settings = useSettings(); const closed = useClosedStatuses(); - const close = ; - if (issue.isPending) return

загрузка {key}…

{close}
; + const close = } onClick={onClose} />; + if (issue.isPending) { + return
{key}{close}

загрузка {key}…

; + } if (issue.error instanceof ApiError && issue.error.status === 404) { - return

Задачи {key} нет в зеркале хаба.

; + return ( +
+ Закрыть}>Задачи {key} нет в зеркале хаба. +
+ ); } if (issue.isError && !issue.data) { return ( -
-

{issue.error.message}{" "} -

- +
+ }> + {issue.error.message} +
); } @@ -37,7 +44,7 @@ export function IssueCard({ issueKey: key, onClose }: { issueKey: string; onClos const due = dueOf(i); const facts = [ i.assignee && {i.assignee}, - due && срок {fmtDate(due)}{isOverdue(due, i.status, closed, todayISO()) && <> просрочено}, + due && срок {fmtDate(due)}{isOverdue(due, i.status, closed, todayISO()) && <> просрочено}, i.sprint && {i.sprint}, i.epic_key && эпик: {i.epic_summary ?? i.epic_key}, ].filter(Boolean); @@ -46,16 +53,15 @@ export function IssueCard({ issueKey: key, onClose }: { issueKey: string; onClos
{i.key} - {i.status && {i.status}} + {i.status && {i.status}} - Открыть в Jira ↗ + Открыть в Jira {close}

{i.summary || i.key}

{facts.length > 0 &&

{facts.flatMap((f, n) => (n ? [" · ", f] : [f]))}

} {issue.isError && ( -

{issue.error.message}{" "} -

+ void issue.refetch()}>Повторить}>{issue.error.message} )}
@@ -64,11 +70,11 @@ export function IssueCard({ issueKey: key, onClose }: { issueKey: string; onClos {i.description && }
-
+ -
+
); @@ -90,7 +96,7 @@ function Links({ issue, linkTypes }: { issue: IssueDetail; linkTypes: Record{" "} {l.summary === null && l.status === null ? нет в зеркале - : <>{l.summary}{l.status && <>{" "}{l.status}}} + : <>{l.summary}{l.status && <>{" "}{l.status}}} ))} @@ -102,7 +108,7 @@ function Links({ issue, linkTypes }: { issue: IssueDetail; linkTypes: Record void }) { - return ; + return ; } function Description({ text, projectKey }: { text: string; projectKey?: string }) { @@ -134,7 +140,7 @@ function Description({ text, projectKey }: { text: string; projectKey?: string } {source ?
} className={cls}>{text}
:
} className={cls}>
} - {(long || open) && } + {(long || open) && } ); } @@ -143,8 +149,8 @@ function Comment({ c, projectKey }: { c: IssueComment; projectKey?: string }) { const [source, setSource] = useState(false); return (
  • -
    - {c.author ?? "—"}{c.created ? `, ${fmtStamp(c.created)}` : ""} +
    + {c.author ?? "—"}{c.created ? `, ${fmtStamp(c.created)}` : ""} setSource(!source)} />
    {source @@ -160,7 +166,7 @@ function Comments({ items, projectKey }: { items: IssueComment[]; projectKey?: s

    Комментарии ({items.length})

    - {items.length > 0 && } + {items.length > 0 && }
    {!items.length &&

    комментариев нет

    } {items.length > 0 && !hidden && ( @@ -194,13 +200,13 @@ function Commands({ issue: i }: { issue: IssueDetail }) {
    Команды моста ({i.commands.length}) - +
    {i.commands.map((c) => ( ))} -
    КомандаПакетТипСтатусЗавершенаОшибка
    #{c.id}#{c.batch_id}{c.type}{c.status} {c.finished_at ? fmtStamp(c.finished_at) : ""}{c.error}
    +
    ); diff --git a/web/src/issues/IssueModal.tsx b/web/src/issues/IssueModal.tsx index a4f7134..f7ac8b6 100644 --- a/web/src/issues/IssueModal.tsx +++ b/web/src/issues/IssueModal.tsx @@ -1,27 +1,25 @@ import { useEffect, useRef } from "react"; +import { Dialog } from "../ui/index.ts"; import { ErrorBoundary } from "../shell/ErrorBoundary.tsx"; import { IssueCard } from "./IssueCard.tsx"; -// имя окна — заголовок карточки; пока его нет (загрузка, 404, ошибка), запасное имя — ключ задачи +// имя окна — ключ задачи; заголовок карточки (h2) подписывает окно через aria-labelledby // окно карточки (design 13): держит фокус и делает фон недоступным; Esc и щелчок по фону — onClose. // Сами окно не закрываем: оно исчезает вместе с адресом карточки. Если браузер закрыл его сам (Esc без активации // пользователя — cancel не отменяемый), открываем снова и просим закрытия по общему пути (блокировщик заметок). export function IssueModal({ issueKey, onClose }: { issueKey: string; onClose: () => void }) { - const ref = useRef(null); // I-2: закрывать щелчком по фону, только если и нажатие было на фоне — выделение текста, отпущенное за краем окна, // даёт click на самом , но нажатие было внутри содержимого const downOnBackdrop = useRef(false); useEffect(() => { - const d = ref.current; - if (d && !d.open) d.showModal(); const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; // страница под окном не прокручивается return () => { document.body.style.overflow = prev; }; }, []); return ( - { if (e.cancelable) { e.preventDefault(); onClose(); } }} - onClose={() => { const d = ref.current; if (d?.isConnected) { d.showModal(); onClose(); } }} + onNativeClose={(e) => { const d = e.currentTarget; if (d.isConnected) { d.showModal(); onClose(); } }} onPointerDown={(e) => { downOnBackdrop.current = e.target === e.currentTarget; }} onClick={(e) => { const fromBackdrop = downOnBackdrop.current; @@ -29,6 +27,6 @@ export function IssueModal({ issueKey, onClose }: { issueKey: string; onClose: ( if (e.target === e.currentTarget && fromBackdrop) onClose(); }}> - + ); } diff --git a/web/src/issues/NoteForm.tsx b/web/src/issues/NoteForm.tsx index e41e9df..1372020 100644 --- a/web/src/issues/NoteForm.tsx +++ b/web/src/issues/NoteForm.tsx @@ -3,6 +3,7 @@ import { useBlocker } from "react-router"; import { useCloseAbort } from "./closeAbort.ts"; import { ApiError } from "../api/client.ts"; import { useSaveNote, useSettings } from "../api/queries.ts"; +import { Button, Field, Notice, Select, Textarea } from "../ui/index.ts"; import { NOTE_FIELDS, type IssueDetail, type NoteField, type NoteFields } from "../api/types.ts"; type Values = Record; @@ -52,33 +53,37 @@ export function NoteForm({ issue, onDone }: { issue: IssueDetail; onDone: () =>
    { e.preventDefault(); submit(); }}>

    Служебные заметки

    - + + {TEXTS.map(([f, label]) => ( -