fix(web): правки по финальному ревью: пометка хаба на доске, кнопка в загрузке, панель данных, контраст предупреждений (#65)
I-1 пометка «Хаб · данные на» убрана с доски; I-2 кнопка в loading сохраняет вид варианта; I-3 статус пакета — постоянный live-регион, не сдвигает кнопку и высоту панели; I-4 текст предупреждений затемнён через color-mix; M-1 точка «агент офлайн» и событие — предупреждение; M-7 ошибка настроек в Notice; M-9 убраны IconGrid и IconReset; M-15 IBM Plex Mono 600; M-16 aria-controls только при раскрытой сетке фильтров. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
04804360d5
commit
0a5ca2e00a
@ -49,7 +49,7 @@ test("«Обновить из Jira» с учебным агентом доход
|
||||
await agentServeRefresh();
|
||||
await expect(page.getByRole("status")).toContainText(/пакет #\d+ выполнен/, { timeout: 15_000 });
|
||||
await expect(btn).toBeEnabled();
|
||||
await expect(page.getByText(/^Хаб · данные на /)).toBeVisible();
|
||||
await expect(page.getByText(/^данные на /)).toBeVisible();
|
||||
await expect(page.locator(".board .card[data-key]")).not.toHaveCount(0);
|
||||
});
|
||||
|
||||
|
||||
@ -39,7 +39,7 @@ describe("страница доски", () => {
|
||||
it("строит доску из хаба: источник, статистика, выбор самого наполненного спринта", async () => {
|
||||
hub();
|
||||
page();
|
||||
expect(await screen.findByText(/^Хаб · данные на/)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/задач из/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/задач из 9/)).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /AWPCA_Спринт 2/ })).toBeInTheDocument();
|
||||
});
|
||||
@ -47,7 +47,7 @@ describe("страница доски", () => {
|
||||
it("снимок запрашивается по проекту страницы", async () => {
|
||||
const fetch = hub();
|
||||
page();
|
||||
await screen.findByText(/^Хаб · данные на/);
|
||||
await screen.findByText(/задач из/);
|
||||
const urls = fetch.mock.calls.map(([u]) => new URL(String(u), "http://hub.test")).filter((u) => u.pathname === "/board");
|
||||
expect(urls[0].searchParams.get("project")).toBe("AWPCA");
|
||||
});
|
||||
@ -120,7 +120,7 @@ describe("страница доски", () => {
|
||||
fireEvent.change(input, { target: { files: [new File([fx("jira-export.csv")], "jira-export.csv", { type: "text/csv" })] } });
|
||||
expect(await screen.findByText("из файла: jira-export.csv")).toBeInTheDocument();
|
||||
await userEvent.click(screen.getByRole("button", { name: "Вернуться к данным хаба" }));
|
||||
expect(await screen.findByText(/^Хаб · данные на/)).toBeInTheDocument();
|
||||
expect(await screen.findByText(/задач из/)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/^из файла:/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -171,7 +171,7 @@ describe("страница доски", () => {
|
||||
page();
|
||||
await screen.findByText(/задач из 9/);
|
||||
expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument();
|
||||
expect(screen.getByText(/^Хаб · данные на/)).toBeInTheDocument();
|
||||
expect(screen.queryByText(/^Хаб · данные на/)).not.toBeInTheDocument(); // данные хаба пометки не имеют (спека, решение 13)
|
||||
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
@ -184,7 +184,7 @@ describe("страница доски", () => {
|
||||
it("T12.3: --head-h снимается при уходе со страницы", async () => {
|
||||
hub();
|
||||
const r = page();
|
||||
await screen.findByText(/^Хаб · данные на/);
|
||||
await screen.findByText(/задач из/);
|
||||
r.unmount();
|
||||
expect(document.documentElement.style.getPropertyValue("--head-h")).toBe("");
|
||||
});
|
||||
@ -192,7 +192,7 @@ describe("страница доски", () => {
|
||||
it("T12.2: переключатель пишет флаг в хранилище один раз, в том числе в StrictMode", async () => {
|
||||
hub();
|
||||
renderWithProviders(<StrictMode><BoardPage project="AWPCA" /></StrictMode>, { route: "/board", path: "/board" });
|
||||
await screen.findByText(/^Хаб · данные на/);
|
||||
await screen.findByText(/задач из/);
|
||||
const set = vi.spyOn(Storage.prototype, "setItem");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Критический путь" }));
|
||||
expect(set.mock.calls.filter(([k]) => k === "graph-crit")).toEqual([["graph-crit", "1"]]);
|
||||
@ -210,12 +210,12 @@ describe("страница доски", () => {
|
||||
hub();
|
||||
localStorage.setItem("graph-pos:old.json", '{"AURORA-9001":{"x":1,"ry":40}}');
|
||||
const r = page("STOCKS_DRRK");
|
||||
await screen.findByText(/^Хаб · данные на/);
|
||||
await screen.findByText(/задач из/);
|
||||
expect(localStorage.getItem("graph-pos:board@STOCKS_DRRK")).toBeNull();
|
||||
expect(localStorage.getItem("graph-pos:board")).toBeNull();
|
||||
r.unmount();
|
||||
page("AWPCA");
|
||||
await screen.findByText(/^Хаб · данные на/);
|
||||
await screen.findByText(/задач из/);
|
||||
expect(localStorage.getItem("graph-pos:board")).toBe('{"AURORA-9001":{"x":1,"ry":40}}');
|
||||
});
|
||||
|
||||
|
||||
@ -42,7 +42,7 @@ export function BoardPage({ project }: { project: string }) {
|
||||
}, []);
|
||||
|
||||
if (settings.isPending) return <p className="hint" data-testid="page-board">Загрузка настроек хаба…</p>;
|
||||
if (settings.error) return <p className="warn" data-testid="page-board">Настройки хаба не загрузились: {settings.error.message}</p>;
|
||||
if (settings.error) return <div data-testid="page-board"><Notice tone="error">Настройки хаба не загрузились: {settings.error.message}</Notice></div>;
|
||||
if (d && d.missing.length) {
|
||||
return (
|
||||
<div data-testid="page-board"><Notice tone="error">
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
.board-src:empty{display:none}
|
||||
.board-src__label{font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--color-text-secondary);max-width:420px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.board-event{font-size:var(--fs-body);line-height:var(--lh-body);color:var(--color-text-secondary)}
|
||||
.board-event--warn{color:var(--color-status-warning)}
|
||||
.board-event--warn{color:color-mix(in srgb, var(--color-status-warning) 55%, var(--color-text-primary))}
|
||||
.menu__label{display:inline-flex;align-items:center;gap:var(--space-2)}
|
||||
.confirm{display:flex;flex-direction:column;gap:var(--space-2);padding:var(--space-2) var(--space-3);font-size:var(--fs-body);line-height:var(--lh-body);max-width:280px}
|
||||
.confirm__row{display:flex;gap:var(--space-2)}
|
||||
|
||||
@ -68,7 +68,7 @@ export function BoardHeader(p: BoardHeaderProps) {
|
||||
<Badge tone="info">из файла: {p.fileName}</Badge>
|
||||
<IconButton label="Вернуться к данным хаба" icon={<IconRefresh size={16} />} onClick={p.onReload} />
|
||||
</>
|
||||
) : <span className="board-src__label" title={p.title}>{p.label}</span>}
|
||||
) : null} {/* данные хаба пометки не имеют: хаб показывает верхняя панель (design.md, решение 13) */}
|
||||
{st && <span className={"board-event" + (st.warn ? " board-event--warn" : "")} role="status">{st.text}</span>}
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
@ -32,7 +32,7 @@ export function IssueFilters({ filters, set, onReset, q, setQ, showClosed, opts,
|
||||
options={[{ value: "open", label: "Открытые" }, { value: "all", label: "Все" }]}
|
||||
onChange={(v) => set("closed", v === "all" ? "1" : "")} />
|
||||
<Button aria-pressed={filters.neighbor === "1"} onClick={() => set("neighbor", filters.neighbor === "1" ? "" : "1")}>Только соседи</Button>
|
||||
<Button icon={<IconFilter />} aria-expanded={open} aria-controls="issue-filters-grid" onClick={toggle}>
|
||||
<Button icon={<IconFilter />} aria-expanded={open} aria-controls={open ? "issue-filters-grid" : undefined} onClick={toggle}>
|
||||
Фильтры{chosen.length > 0 && <Badge tone="info">{chosen.length}</Badge>}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@ -5,6 +5,7 @@ import "@fontsource/mulish/400.css";
|
||||
import "@fontsource/mulish/600.css";
|
||||
import "@fontsource/mulish/700.css";
|
||||
import "@fontsource/ibm-plex-mono/500.css";
|
||||
import "@fontsource/ibm-plex-mono/600.css";
|
||||
import { App } from "./App.tsx";
|
||||
import { applyTheme, readTheme } from "./shell/theme.ts";
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ export function DataStatus({ project }: { project: string | null }) {
|
||||
title="Поставить в хаб пакет «обновление по базовой JQL» и дождаться его: агент перечитает задачи из Jira">
|
||||
Обновить из Jira
|
||||
</Button>
|
||||
{rt && <span className={`data-status__event data-status__event--${rt.tone}`} role="status">{rt.text}</span>}
|
||||
<span className={`data-status__event${rt ? ` data-status__event--${rt.tone}` : ""}`} role="status" title={rt?.text}>{rt?.text}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import { refreshText } from "./refreshStatus.ts";
|
||||
describe("refreshText", () => {
|
||||
it("пакет: офлайн, очередь, остаток, повтор", () => {
|
||||
expect(refreshText({ kind: "batch", id: 7, phase: "queued", remaining: 12, agentOffline: true, retrying: false }))
|
||||
.toEqual({ text: "агент офлайн — пакет выполнится, когда он выйдет на связь; пакет #7 в очереди, осталось ~12 с…", tone: "info" });
|
||||
.toEqual({ text: "агент офлайн — пакет выполнится, когда он выйдет на связь; пакет #7 в очереди, осталось ~12 с…", tone: "warning" });
|
||||
expect(refreshText({ kind: "batch", id: 7, phase: "running", remaining: 0, agentOffline: false, retrying: true }))
|
||||
.toEqual({ text: "пакет #7 выполняется… (хаб не отвечает, повтор через 3 с)", tone: "warning" });
|
||||
});
|
||||
|
||||
@ -20,7 +20,7 @@ export function refreshText(e: RefreshEvent): { text: string; tone: RefreshTone
|
||||
const off = e.agentOffline ? "агент офлайн — пакет выполнится, когда он выйдет на связь; " : "";
|
||||
const left = e.remaining ? `, осталось ~${e.remaining} с` : "";
|
||||
const retry = e.retrying ? ` (хаб не отвечает, повтор через ${POLL_MS / 1000} с)` : "";
|
||||
return { text: `${off}пакет #${e.id} ${e.phase === "queued" ? "в очереди" : "выполняется"}${left}…${retry}`, tone: e.retrying ? "warning" : "info" };
|
||||
return { text: `${off}пакет #${e.id} ${e.phase === "queued" ? "в очереди" : "выполняется"}${left}…${retry}`, tone: e.retrying || e.agentOffline ? "warning" : "info" };
|
||||
}
|
||||
case "batch-done": return { text: `пакет #${e.id} выполнен`, tone: "success" };
|
||||
case "batch-failed": return { text: `пакет #${e.id} не выполнен: ${e.error}`, tone: "error" };
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
/* оболочка: верхняя панель разделов (design.md, решение 9) */
|
||||
.appbar{
|
||||
position:relative;
|
||||
display:flex;align-items:center;flex-wrap:wrap;gap:8px 16px;min-height:64px;padding:8px 24px;box-sizing:border-box;
|
||||
border-bottom:1px solid var(--color-border-default);background:var(--color-bg-surface);
|
||||
}
|
||||
@ -7,16 +8,21 @@
|
||||
.appbar__spacer{flex:1 1 auto}
|
||||
.data-status{display:flex;align-items:center;flex-wrap:wrap;gap:4px 12px;font-size:var(--fs-caption);line-height:var(--lh-caption);color:var(--color-text-secondary)}
|
||||
.data-status__agent{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
|
||||
.data-status__dot{width:8px;height:8px;border-radius:50%;background:var(--color-status-error)}
|
||||
.data-status__dot{width:8px;height:8px;border-radius:50%;background:var(--color-status-warning)}
|
||||
.data-status__dot.is-online{background:var(--color-status-success)}
|
||||
.data-status__sync{white-space:nowrap}
|
||||
.data-status__event{font-weight:600}
|
||||
/* I-3: живой регион всегда в DOM; ≥1100 текст события — отдельной строкой у нижней границы панели (кнопка не двигается, высота панели не меняется) */
|
||||
.data-status__event{font-weight:600;min-width:0}
|
||||
.data-status__event:empty{position:absolute}
|
||||
.data-status__event--info{color:var(--color-text-secondary)}
|
||||
.data-status__event--success{color:var(--color-status-success)}
|
||||
.data-status__event--warning{color:var(--color-status-warning)}
|
||||
.data-status__event--warning{color:color-mix(in srgb, var(--color-status-warning) 55%, var(--color-text-primary))}
|
||||
.data-status__event--error{color:var(--color-status-error)}
|
||||
.crash{max-width:640px;margin:48px auto}
|
||||
.crash h2{margin:0;font:inherit}
|
||||
@media (min-width:1100px){
|
||||
.data-status__event{position:absolute;right:24px;bottom:1px;max-width:min(560px,45vw);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:14px}
|
||||
}
|
||||
@media (max-width:1099px){
|
||||
.appbar .data-status{order:10;flex:1 0 100%}
|
||||
.appbar .ui-theme,.appbar .more{order:4} /* T10.1: тема и «…» остаются в первой строке, статус данных — второй */
|
||||
|
||||
@ -30,7 +30,5 @@ export const IconCritical = make(<><path d="M13 3L5 13.5h6L10 21l8-10.5h-6z"/></
|
||||
export const IconCalendar = make(<><rect x="4" y="5" width="16" height="15" rx="2"/><path d="M4 10h16M8 3v4M16 3v4"/></>);
|
||||
export const IconNeighbors = make(<><circle cx="12" cy="12" r="2.5"/><circle cx="5" cy="6" r="2"/><circle cx="19" cy="6" r="2"/><circle cx="12" cy="20" r="1.5"/><path d="M10 10.5L6.5 7.5M14 10.5l3.5-3M12 14.5v4"/></>);
|
||||
export const IconLink = make(<><path d="M10 14a4 4 0 0 0 5.7 0l3-3a4 4 0 0 0-5.7-5.7l-1 1"/><path d="M14 10a4 4 0 0 0-5.7 0l-3 3a4 4 0 0 0 5.7 5.7l1-1"/></>);
|
||||
export const IconGrid = make(<><rect x="4" y="4" width="6.5" height="6.5" rx="1"/><rect x="13.5" y="4" width="6.5" height="6.5" rx="1"/><rect x="4" y="13.5" width="6.5" height="6.5" rx="1"/><rect x="13.5" y="13.5" width="6.5" height="6.5" rx="1"/></>);
|
||||
export const IconReset = make(<><path d="M4 12a8 8 0 1 1 2.3 5.7"/><path d="M4 19v-7h7"/></>);
|
||||
export const IconLegend = make(<><path d="M9 6h11M9 12h11M9 18h11"/><circle cx="4.5" cy="6" r=".8"/><circle cx="4.5" cy="12" r=".8"/><circle cx="4.5" cy="18" r=".8"/></>);
|
||||
export const IconOpen = make(<><path d="M5 12h14"/><path d="M13 6l6 6-6 6"/></>);
|
||||
|
||||
@ -26,6 +26,14 @@
|
||||
.ui-btn:disabled { cursor: not-allowed; color: var(--color-text-disabled); }
|
||||
.ui-btn--primary:disabled, .ui-btn--danger:disabled { background: var(--color-bg-track); border-color: var(--color-border-default); }
|
||||
.ui-btn[aria-busy="true"] { cursor: progress; }
|
||||
/* загрузка: вид своего варианта, а не серая «выключенная» кнопка (AURA); повторный щелчок невозможен — disabled */
|
||||
.ui-btn--primary[aria-busy="true"]:disabled { background: var(--color-action-primary); border-color: transparent; color: var(--color-text-on-primary); }
|
||||
.ui-btn--danger[aria-busy="true"]:disabled { background: var(--color-action-danger); border-color: transparent; color: var(--color-text-on-primary); }
|
||||
.ui-btn--secondary[aria-busy="true"]:disabled { color: var(--color-text-primary); }
|
||||
.ui-btn--ghost[aria-busy="true"]:disabled { color: var(--color-action-primary); }
|
||||
.ui-btn--primary[aria-busy="true"] .ui-spinner, .ui-btn--danger[aria-busy="true"] .ui-spinner {
|
||||
border-color: color-mix(in srgb, var(--color-text-on-primary) 35%, transparent); border-top-color: var(--color-text-on-primary);
|
||||
}
|
||||
.ui-spinner {
|
||||
width: 16px; height: 16px; border-radius: var(--radius-round);
|
||||
border: 2px solid var(--color-border-strong); border-top-color: var(--color-action-primary);
|
||||
@ -72,7 +80,7 @@
|
||||
.ui-badge--neutral { background: var(--color-bg-track); border-color: var(--color-border-default); color: var(--color-text-secondary); }
|
||||
.ui-badge--success { background: var(--color-status-success-bg); border-color: var(--color-status-success); color: var(--color-status-success); }
|
||||
.ui-badge--error { background: var(--color-status-error-bg); border-color: var(--color-status-error); color: var(--color-status-error); }
|
||||
.ui-badge--warning { background: var(--color-status-warning-bg); border-color: var(--color-status-warning-border); color: var(--color-status-warning); }
|
||||
.ui-badge--warning { background: var(--color-status-warning-bg); border-color: var(--color-status-warning-border); color: color-mix(in srgb, var(--color-status-warning) 55%, var(--color-text-primary)); }
|
||||
.ui-badge--info { background: var(--color-bg-subtle); border-color: var(--color-border-strong); color: var(--color-text-primary); }
|
||||
|
||||
.ui-chip {
|
||||
@ -99,6 +107,7 @@
|
||||
.ui-notice__text { font-size: var(--fs-caption); line-height: var(--lh-caption); color: var(--color-text-primary); }
|
||||
.ui-notice__action { flex: none; }
|
||||
.ui-notice a { color: var(--color-action-primary); }
|
||||
.ui-notice--warning a { color: var(--color-text-primary); text-decoration: underline; }
|
||||
.ui-notice--success { background: var(--color-status-success-bg); border-color: var(--color-status-success); }
|
||||
.ui-notice--success .ui-notice__icon { background: var(--color-status-success); }
|
||||
.ui-notice--error { background: var(--color-status-error-bg); border-color: var(--color-status-error); }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user