Фиксы финального ревью: страница не шире окна (C-1), событие пакета на телефоне (I-1) (#69)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-10-22 09:13:51 +04:00
parent 1f74a27cb3
commit d3db88708b
8 changed files with 51 additions and 6 deletions

View File

@ -83,3 +83,9 @@
| 4 | 22.10.2026 | Свёртка панели в полосу 64 px с запоминанием (решение 3) | доске нужна ширина; раз в спринт граф — тогда удобно свернуть | низкая |
| 5 | 22.10.2026 | На узком экране — строка с «Меню» и выезжающая панель (решение 4) | закреплённая панель 4–5 строк (M-12 #65) съела бы экран | низкая: узкий экран — только «без поломок» |
| 6 | 22.10.2026 | Развёрнута по умолчанию, ширина 240 px | на 1920 место есть; название проекта видно целиком | низкая |
| 7 | 22.10.2026 | R-1: панель без `overflow-y:auto` (вопреки решению 1) | прокрутка панели обрезала бы меню «Ещё» свёрнутой панели, выходящее за 64 px | низкая: на окне ниже ~470 px низ панели обрезан (M-5) |
| 8 | 22.10.2026 | R-2: в выезжающей панели «Закрыть меню» вместо «Свернуть панель» | свёртка внутри выезжающей панели бессмысленна (решение 4) | нет |
| 9 | 22.10.2026 | R-3: e2e M-3 переходит по разделам обычным щелчком, не `dispatchEvent` | панель закреплена, щелчок больше не прокручивает доску; тест заодно проверяет закрепление | нет |
| 10 | 22.10.2026 | R-4 (C-1): `.shell>.page{width:100%}` и `.page` — контейнер (`container: page/inline-size`); пороги раскладки страниц (фильтры списка, прокрутка таблицы, колонка ящика «Проблемы») — запросами к контейнеру с порогами «прежний порог окна минус поля страницы 48 px» | рядом панель 240/64 px: ширина страницы больше не равна ширине окна, и пороги по окну давали таблицу шире страницы | низкая: пороги — числа в CSS |
| 11 | 22.10.2026 | R-5 (I-1): на узком экране событие пакета — строкой под закреплённой строкой (`aria-hidden`, озвучивает `role=status` кнопки) | на телефоне подсказок нет, результат «Обновить из Jira» был не виден | низкая: строка занимает 1–2 строки по высоте, пока есть событие |

View File

@ -42,3 +42,19 @@ test("390 px: «Меню» открывает панель, «Задачи» з
await expect(page.getByTestId("page-issues")).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
});
test("C-1: на средних ширинах страница не шире окна — «Задачи» на 1280, доска на 1000 прокручивается сама", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await login(page, "/app/issues");
await expect(page.locator("table.issue-table tbody tr").first()).toBeVisible();
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(1280);
await page.setViewportSize({ width: 1000, height: 800 });
await page.getByRole("link", { name: "Доска связей" }).click();
await expect(page.locator(".board .card[data-key]").first()).toBeVisible();
await page.getByRole("button", { name: "По датам" }).click();
await expect(page.locator(".dcol").first()).toBeVisible();
await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(1000);
await expect(page.getByRole("button", { name: /Проблемы/ })).toBeInViewport();
const wrap = page.locator(".wrap");
expect(await wrap.evaluate((el) => el.scrollWidth > el.clientWidth)).toBe(true); // доска шире своей рамки — прокрутка внутри
});

View File

@ -167,7 +167,7 @@ td.k{font-family:var(--font-mono);font-size:12px;white-space:nowrap;color:var(--
/* 9. Движение и узкие экраны */
@media (prefers-reduced-motion:no-preference){.card{transition:border-color .15s}}
@media (prefers-reduced-motion:reduce){.card .grip,.fold svg{transition:none}}
@media (max-width:900px){.stage{flex-direction:column}.drawer{flex:none;max-width:100%;max-height:none;position:static}}
@container page (max-width:852px){.stage{flex-direction:column}.drawer{flex:none;max-width:100%;max-height:none;position:static}}
@media (max-width:640px){.sticky{position:static}.dates-bar{position:static}}
@media (max-width:640px){.board-controls__sep,.legend .sep{display:none}}

View File

@ -7,10 +7,10 @@
.issue-filters__row .ui-btn[aria-pressed="true"],.issue-filters__row .ui-btn[aria-expanded="true"]{border-color:var(--color-border-selected);color:var(--color-action-primary)}
.issue-filters__chips{display:flex;flex-wrap:wrap;align-items:center;gap:var(--space-2)}
.issue-filters__grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--space-3) var(--space-4)}
@media (max-width:899.98px){.issue-filters__grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media (max-width:639.98px){.issue-filters__grid{grid-template-columns:minmax(0,1fr)}.issue-filters__search{max-width:none}}
/* прокрутка внутри панели — только на узких экранах: overflow на предке отключает липкую шапку страницы */
@media (max-width:1099.98px){.issue-table-wrap{overflow-x:auto}}
@container page (max-width:851.98px){.issue-filters__grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
@container page (max-width:607.98px){.issue-filters__grid{grid-template-columns:minmax(0,1fr)}.issue-filters__search{max-width:none}}
/* прокрутка внутри панели — только при узкой странице: overflow на предке отключает липкую шапку страницы; пороги контейнера = прежние пороги окна минус поля страницы */
@container page (max-width:1051.98px){.issue-table-wrap{overflow-x:auto}}
.issue-table th:first-child{border-top-left-radius:calc(var(--radius-xl) - 1px)}
.issue-table th:last-child{border-top-right-radius:calc(var(--radius-xl) - 1px)}
.issue-table tbody tr:last-child td:first-child{border-bottom-left-radius:calc(var(--radius-xl) - 1px)}

View File

@ -228,4 +228,15 @@ describe("боковая панель на узком экране", () => {
await userEvent.click(within(bar()).getByRole("button", { name: "Закрыть меню" }));
expect(screen.queryByRole("complementary", { name: "Панель приложения" })).toBeNull();
});
it("I-1: событие пакета видно под строкой, не только в подсказке", async () => {
hub();
open("/app/board?project=AWPCA");
const top = await screen.findByRole("banner", { name: "Строка приложения" });
await userEvent.click(within(top).getByRole("button", { name: "Обновить из Jira" }));
const strip = await screen.findByTestId("topbar-event");
await waitFor(() => expect(strip).toHaveTextContent(/пакет #7 выполняется/));
expect(strip).not.toHaveClass("visually-hidden");
expect(strip).toHaveAttribute("aria-hidden", "true"); // озвучивает role=status кнопки, второй раз не нужно
});
});

View File

@ -6,6 +6,8 @@ import { withProject } from "../projects/current.ts";
import { ProjectPicker } from "../projects/ProjectPicker.tsx";
import { IconBoard, IconButton, IconClose, IconList, IconMenu, IconSidebar, Menu, MenuItem, ThemeSwitch } from "../ui/index.ts";
import { DataStatus } from "./DataStatus.tsx";
import { useJiraRefresh } from "./refresh.ts";
import { refreshText } from "./refreshStatus.ts";
import { readSidebar, saveSidebar } from "./sidebarPref.ts";
export type Section = "/board" | "/issues";
@ -37,6 +39,8 @@ function NarrowShell(p: Props) {
const [open, setOpen] = useState(false);
const menuBtn = useRef<HTMLButtonElement>(null);
// объект стабилен, пока панель открыта: эффект фокуса и Escape в Panel не перезапускается на каждой отрисовке
const refresh = useJiraRefresh();
const rt = refresh.event ? refreshText(refresh.event) : null; // I-1: на телефоне подсказок нет — событие пакета строкой под панелью
const drawer = useMemo(() => ({ close: () => { setOpen(false); menuBtn.current?.focus(); }, navigated: () => setOpen(false) }), []);
return (
<>
@ -47,6 +51,7 @@ function NarrowShell(p: Props) {
<span className="sidebar__spacer" />
<DataStatus project={p.project} compact />
</header>
{rt && <div className={`topbar-event data-status__event--${rt.tone}`} data-testid="topbar-event" aria-hidden="true">{rt.text}</div>}
{open && <>
<div className="sidebar-backdrop" data-testid="sidebar-backdrop" onClick={drawer.close} />
<Panel {...p} drawer={drawer} />

View File

@ -1,5 +1,7 @@
/* оболочка: боковая панель и страница (#69, design 1–3); прокрутка — у окна, панель липнет к его верху */
.shell{display:grid;grid-template-columns:auto minmax(0,1fr);min-height:100vh}
/* C-1: у элемента сетки с margin-inline:auto ширина — по содержимому; явная 100% держит страницу в своей дорожке */
.shell>.page{width:100%;box-sizing:border-box}
.sidebar{
position:sticky;top:0;height:100vh;height:100dvh;width:240px;box-sizing:border-box;z-index:20;
display:flex;flex-direction:column;gap:var(--space-4);padding:var(--space-4);
@ -58,3 +60,7 @@
.shell{display:block}
:root{--topbar-h:56px}
}
.topbar-event{
position:sticky;top:56px;z-index:19;padding:var(--space-2) var(--space-4);border-bottom:1px solid var(--color-border-default);
background:var(--color-bg-subtle);font-size:var(--fs-caption);line-height:var(--lh-caption);font-weight:600;overflow-wrap:anywhere;
}

View File

@ -3,7 +3,8 @@ html,body{min-height:100%}
body{font:var(--fs-body)/var(--lh-body) var(--font-ui);background:var(--color-bg-canvas);color:var(--color-text-primary);margin:0}
h1{font-size:var(--fs-section);line-height:var(--lh-section);font-weight:700;margin:0}
[hidden]{display:none!important}
.page{padding-inline:24px;padding-block:0 32px;max-width:1440px;margin-inline:auto}
/* страница — контейнер: раскладка страниц зависит от её ширины, а не окна (рядом боковая панель, #69) */
.page{padding-inline:24px;padding-block:0 32px;max-width:1440px;margin-inline:auto;container:page/inline-size}
.page--board{max-width:none}
@media (max-width:639px){.page{padding-inline:16px}}
:focus-visible{outline:2px solid var(--color-focus-ring);outline-offset:2px}