From cabf8d75f47885d2bffa198178ad8f9a2be3d067 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Thu, 22 Oct 2026 08:48:48 +0400 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D1=81=D0=BA=D0=B0:=20=D0=B1=D0=B5?= =?UTF-8?q?=D0=B7=20=D1=87=D0=B5=D1=80=D0=B5=D0=B4=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B4=D0=BE=D1=80=D0=BE=D0=B6=D0=B5=D0=BA,=20?= =?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=81=D1=80=D0=BE=D1=87=D0=BA=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87=D0=BA=D0=B5=20(#6?= =?UTF-8?q?9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- web/src/board/board.css | 2 +- web/src/board/components/BoardCanvas.tsx | 2 +- web/src/board/components/Card.tsx | 9 ++++--- web/src/board/components/CardLate.test.tsx | 30 ++++++++++++++++++++++ web/src/styles/base.css | 2 ++ web/src/styles/tokens.css | 3 --- web/src/styles/tokens.test.ts | 7 +++++ 7 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 web/src/board/components/CardLate.test.tsx diff --git a/web/src/board/board.css b/web/src/board/board.css index f7ea935..253c5f4 100644 --- a/web/src/board/board.css +++ b/web/src/board/board.css @@ -50,7 +50,6 @@ svg.guides line{stroke:var(--guide);stroke-width:1;stroke-dasharray:4 3} .empty{padding:40px 16px;color:var(--color-text-secondary);text-align:center} #hubState{white-space:normal;min-width:0} .lane{position:absolute;left:0;right:0;border-top:1px solid var(--color-border-default)} -.lane:nth-child(even of .lane){background:var(--lane-alt)} .lane.folded{background:var(--color-bg-track);border-bottom:1px solid var(--color-border-strong);border-top-color:var(--color-border-strong)} .lane.target{box-shadow:inset 0 3px 0 var(--color-action-primary)} .lane.target-after{box-shadow:inset 0 -3px 0 var(--color-action-primary)} @@ -134,6 +133,7 @@ svg.guides line{stroke:var(--guide);stroke-width:1;stroke-dasharray:4 3} /* статус — вид Badge (neutral) */ .status{font-size:var(--fs-caption);line-height:var(--lh-caption);font-weight:600;padding:1px 8px;border:1px solid var(--color-border-default);border-radius:var(--radius-round);background:var(--color-bg-track);color:var(--color-text-secondary);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} .due{font-variant-numeric:tabular-nums;font-weight:600;flex:0 0 auto} +.due.late{color:var(--color-status-error)} .who::before{content:"";display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--color-action-primary);margin-right:5px;vertical-align:1px} .who.none{color:var(--color-text-secondary);font-style:italic} .who.none::before{background:transparent;border:1px solid var(--color-text-secondary)} diff --git a/web/src/board/components/BoardCanvas.tsx b/web/src/board/components/BoardCanvas.tsx index 9d14d04..c90cca8 100644 --- a/web/src/board/components/BoardCanvas.tsx +++ b/web/src/board/components/BoardCanvas.tsx @@ -190,7 +190,7 @@ export function BoardCanvas({ vis, byDate, snap, today, store, sprintSel, emptyT ))} {Object.entries(placed.pos).filter(([k]) => byKey[k]).map(([k, box]) => ( + closed={closedKeys.has(k)} today={today} onPointerDown={onCardDown} onAutoSize={onAutoSize} /> ))} diff --git a/web/src/board/components/Card.tsx b/web/src/board/components/Card.tsx index c94ce92..5c22a8a 100644 --- a/web/src/board/components/Card.tsx +++ b/web/src/board/components/Card.tsx @@ -14,6 +14,7 @@ export interface CardProps { multiSprint: boolean; sprintSel: Set; closed?: boolean; // задача закрыта (статус из closed_statuses) — карточка приглушена + today?: string; // ISO-дата сегодня: незакрытая задача со сроком раньше — просрочена (#69) onPointerDown?: (ev: PointerEvent, key: string) => void; onAutoSize?: (key: string) => void; } @@ -21,13 +22,14 @@ export interface CardProps { // T11.1: при перетаскивании одной карточки place() создаёт новые box у всех — сравниваем box по значению export function cardPropsEqual(a: CardProps, b: CardProps): boolean { const sameBox = a.box === b.box || (!!a.box && !!b.box && a.box.x === b.box.x && a.box.y === b.box.y && a.box.w === b.box.w && a.box.h === b.box.h); - return sameBox && a.node === b.node && a.rel === b.rel && a.multiSprint === b.multiSprint && a.sprintSel === b.sprintSel && !!a.closed === !!b.closed && + return sameBox && a.node === b.node && a.rel === b.rel && a.multiSprint === b.multiSprint && a.sprintSel === b.sprintSel && !!a.closed === !!b.closed && a.today === b.today && a.onPointerDown === b.onPointerDown && a.onAutoSize === b.onAutoSize; } -export const Card = memo(function Card({ node: n, box, rel, multiSprint, sprintSel, closed, onPointerDown, onAutoSize }: CardProps) { +export const Card = memo(function Card({ node: n, box, rel, multiSprint, sprintSel, closed, today, onPointerDown, onAutoSize }: CardProps) { const cls = "card" + (n.dim || n.stub ? " out" : "") + (closed ? " closed" : "") + (n.crit ? " crit" : "") + (n.focus ? " focus" : ""); const style = box ? { left: box.x, top: box.y, width: box.w, height: box.h } : { width: CW }; + const late = !closed && !!n.due && !!today && n.due < today; const showTag = !n.stub && (multiSprint || n.dim); // метка спринта — если спринтов на доске больше одного или это сосед return (
{n.status && {n.status}} - {n.due && до {fmtDate(n.due)}} + {n.due && + до {fmtDate(n.due)}{late && , просрочено}}
)} {!n.stub &&
{shortName(n.who)}
} diff --git a/web/src/board/components/CardLate.test.tsx b/web/src/board/components/CardLate.test.tsx new file mode 100644 index 0000000..7468968 --- /dev/null +++ b/web/src/board/components/CardLate.test.tsx @@ -0,0 +1,30 @@ +import { render } from "@testing-library/react"; +import { MemoryRouter } from "react-router"; +import { describe, expect, it } from "vitest"; +import { Card, cardPropsEqual, type CardProps } from "./Card.tsx"; + +const node = { key: "A-1", title: "Т", kind: "разработка", prio: "Средний", status: "В работе", due: "2026-10-20", who: "Иванов И. И." } as CardProps["node"]; +const show = (p: Partial) => render( + , +).container.querySelector(".due")!; + +describe("просрочка на карточке доски (#69, design 10)", () => { + it("незакрытая задача с прошедшим сроком — дата цветом ошибки, «просрочено» в подсказке и тексте", () => { + const due = show({}); + expect(due.classList.contains("late")).toBe(true); + expect(due.getAttribute("title")).toBe("просрочено"); + expect(due.textContent).toContain("просрочено"); + }); + it("закрытая задача не просрочена", () => { + expect(show({ closed: true }).classList.contains("late")).toBe(false); + }); + it("срок сегодня — ещё не просрочено", () => { + const due = show({ node: { ...node, due: "2026-10-22" } }); + expect(due.classList.contains("late")).toBe(false); + expect(due.textContent).not.toContain("просрочено"); + }); + it("смена дня перерисовывает карточку", () => { + const base: CardProps = { node, multiSprint: false, sprintSel: new Set(), today: "2026-10-20" }; + expect(cardPropsEqual(base, { ...base, today: "2026-10-21" })).toBe(false); + }); +}); diff --git a/web/src/styles/base.css b/web/src/styles/base.css index 54a8a42..202ce5c 100644 --- a/web/src/styles/base.css +++ b/web/src/styles/base.css @@ -8,3 +8,5 @@ h1{font-size:var(--fs-section);line-height:var(--lh-section);font-weight:700;mar @media (max-width:639px){.page{padding-inline:16px}} :focus-visible{outline:2px solid var(--color-focus-ring);outline-offset:2px} @media (prefers-reduced-motion: reduce){*{transition:none!important;animation:none!important}} +/* текст только для экранного чтения */ +.visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0} diff --git a/web/src/styles/tokens.css b/web/src/styles/tokens.css index 4590601..32179c2 100644 --- a/web/src/styles/tokens.css +++ b/web/src/styles/tokens.css @@ -43,7 +43,6 @@ --p-low: #a3a39b; --edge: #a3a39b; --guide: #c2410c; - --lane-alt: #f5f5f2; --edge-strong: var(--color-action-primary); --space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px; --space-5: 20px; --space-6: 24px; --space-7: 28px; --space-8: 32px; @@ -100,7 +99,6 @@ --p-low: #6c726b; --edge: #6c726b; --guide: #f08a5a; - --lane-alt: #252525; --edge-strong: var(--color-action-primary); } } @@ -147,6 +145,5 @@ --p-low: #6c726b; --edge: #6c726b; --guide: #f08a5a; - --lane-alt: #252525; --edge-strong: var(--color-action-primary); } diff --git a/web/src/styles/tokens.test.ts b/web/src/styles/tokens.test.ts index ab0e13b..024d0c4 100644 --- a/web/src/styles/tokens.test.ts +++ b/web/src/styles/tokens.test.ts @@ -63,3 +63,10 @@ describe("токены AURA", () => { expect(block(css, ':root[data-theme="dark"]')).toContain("color-scheme: dark"); }); }); + +describe("доска без чередования дорожек (#69, design 9)", () => { + it("токена --lane-alt нет, правила чётной дорожки нет", () => { + expect(readFileSync(TOKENS, "utf8")).not.toContain("--lane-alt"); + expect(readFileSync(resolve(SRC, "board/board.css"), "utf8")).not.toMatch(/\.lane:nth-child/); + }); +});