fix(roadmap): UX backlog — легенда, touch, collision, kbd-nav, skeleton, a11y#160
Merged
Conversation
Задачи без startDate получали start = end = dueDate → ширина бара 0 → Math.max(6, 0) = 6px → вертикальная черта вместо нормального элемента. Решение: задачи с только dueDate (без startDate) теперь рендерятся как ромб-маркер (milestone diamond) — стандарт для дедлайнов в Gantt-диаграммах. Ромб поддерживает hover-тултип, анимацию просрочки и работает на всех уровнях зума (week/month/quarter). Тултип обновлён: показывает «Дедлайн: X» вместо диапазона дат для milestone-задач.
Frontend (RoadmapView.tsx): - fix: разделить positioning и scale transforms (HIGH code) — wrapper держит translateY(-50%), inner div держит rotate/scale → hover не ломает позицию - fix: child-ромб минимум 14px вместо 12 (HIGH ux) - fix: hitbox ромба 32px вместо 16 — легче попасть мышью (HIGH ux) - fix: clampedMx — ромб не вылезает за левый/правый край (CRITICAL ux) - fix: иконка в тултипе milestone — ромб SVG вместо галочки (HIGH ux) - fix: hint «Добавьте дату начала» в тултипе milestone (MEDIUM ux) - fix: «нет дат» перенесено на left:10, было right:10 (LOW ux) - fix: safeColor() — валидация status.color перед inline CSS (MEDIUM security) - fix: prefers-reduced-motion @media в <style> (MEDIUM ux) Backend (boards.service.ts): - fix: statusHistory select — только нужные поля (LOW security) - fix: MAX_RANGE_DAYS=730 — защита от слишком широкого диапазона дат (LOW security) Specs: - docs: specs/roadmap-milestone.md — полная спека с backlog открытых задач
…ollision, kbd-nav, rAF, scroll-fade, overdue badge
…legend a11y, left fade, BDD+SDD спеки - Backend: parseIsoDate() — 400 на невалидную дату; toDate <= fromDate — 400 до проверки MAX_RANGE_DAYS - Frontend touch tooltip: touchmove close listener + 4000ms auto-hide + touchTimerRef (milestone, bar, badge) - Legend: aria-haspopup="true"; добавить строки сегментного бара и off-screen badge - Scroll affordance: симметричный left-fade (зеркально к right-fade) - Overdue badge: onMouseEnter/onMouseLeave/onTouchStart → показывает full tooltip при наведении - Устранить today-shadowing в ghost-tail IIFE (использовать outer today) - Specs: roadmap-ux-backlog.feature (Gherkin, 30 сценариев) + roadmap-ux-backlog-sdd.md (SDD, 8 разделов)
Код-ревью:
- touchClearRef: removeEventListener перед каждым новым touchstart, + в cleanup useEffect и в timeout callback — устранена утечка {once:true} listener
- Collision cluster: pairwise сравнение pts[j].mx - pts[j-1].mx < 22 вместо якорного pts[i].mx — корректно для 3+ milestone
- milestoneYOffsets завёрнут в useMemo([rows, dayPx]) — не пересчитывать при hover
- today передаётся в BarTooltip пропом — убрана независимая getToday() в компоненте
- Keyboard guard: e.metaKey || e.ctrlKey || e.altKey — не перехватывать системные шоткаты
- quarter branch renderHeader: const t = getToday() один раз вместо 9 вызовов в цикле
ИБ-ревью:
- parseIsoDate: strict ISO regex /^\d{4}-\d{2}-\d{2}(T[\d:.Z+-]+)?$/ перед new Date()
- toDate < fromDate (строго, single-day range допустим)
- SAFE_COLOR_RE: \s* → [ \t]* — запрет управляющих символов в функциональной нотации
UX/a11y:
- [role="button"]:focus-visible { outline: 2px solid #4F6EF7 } в style-блоке — надёжный focus-индикатор без imperative мутации
- prefers-reduced-motion: добавить transition:none!important
- Chevron: tabIndex={-1} + aria-hidden="true" — устранить двойную интерактивность
- Skeleton: role="status" + aria-label + aria-busy={loading}
- Legend button: aria-haspopup="dialog" (не "true")
- aria-live="polite" span: анонс смены масштаба для screen reader
- Overdue badge: role="img" + aria-label
Backend:
- parseIsoDate → module scope, generic error message (не отдаёт user input в ответе)
- MAX_ROADMAP_RANGE_DAYS — именованная константа
- logEvent .catch() в createBoard/updateBoard/deleteBoard — audit log не прерывает основную операцию
- updateBoard: явный список полей вместо dto spread → защита от mass-assignment
- getBoardByPrefix: убран /i флаг (валидируем после .toUpperCase())
- take:100 в getBoard прокомментирован
Frontend — код:
- parseDate: strict ISO regex перед new Date()
- historySegs: parseDate() для seg.startedAt/endedAt вместо голого new Date() — защита от NaN px
- window.innerWidth/Height: guard для SSR/embedded (fallback 1280/800)
- handleTouch: единый useCallback — убрана 30-строчная трипликация onTouchStart
- rowYOffsets useMemo → yOffset = rowYOffsets[idx] вместо O(n²) reduce
- legendPopoverRef: focus на popover при открытии (useEffect)
- assignee: имя ограничено 40 символами, fallback '?' при пустой строке
- isMilestone: используется константа в ветке рендера вместо inline !start && end
- historySegs key: `${seg.name}-${i}` вместо index
- legend rows key: row.label вместо index
Frontend — a11y:
- aria-atomic="true" на zoom live region
- role="group" + aria-label на zoom button group
- aria-pressed на zoom buttons и hide-open toggle
- aria-hidden="true" на Today SVG, eye SVGs, BarTooltip decorative SVGs, priority dot
- aria-live="polite" + aria-atomic на task count span
- Legend popup: role="dialog" + aria-modal + aria-labelledby="rm-legend-heading" + tabIndex=-1 + ref focus
- id="rm-legend-heading" на заголовок легенды
- aria-hidden на backdrop div
- role="status" + aria-label на skeleton, role="status" + aria-live на empty state
- role="presentation" на today line
- Milestone diamond: tabIndex=0, role="button", aria-label, onFocus/onBlur tooltip
- Overdue tail: role="img" + aria-label
- data-roadmap-root: prefers-reduced-motion scoped к компоненту (не глобально)
- button:focus-visible{ outline } в style block
# Conflicts: # frontend/src/components/RoadmapView.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Закрывает все открытые задачи из backlog-спеки roadmap-milestone.md (U1–U9, C1–C4, S1) и добавляет BDD + SDD-документацию.
Frontend (
RoadmapView.tsx)?-кнопка): popover с 5 строками (бар, milestone-ромб, сегментный бар, просрочка, off-screen badge) + kbd shortcutsonTouchStart+ 4 с auto-hide +touchmoveclose;touchClearRefдля корректного cleanup без утечки listenerrole="button",tabIndex={0},aria-expanded,aria-labelна строках с подзадачамиrole="img"+aria-labeltoday = getToday()вычисляется один раз перед рендером и передаётся вBarTooltipпропомonMouseMove → rAF throttle— нет 60fps ре-рендераisMilestoneвынесен какconstвBarTooltiptask.titleвtitle-атрибуте ограничен 200 символамиBackend (
boards.service.ts)parseIsoDate(): strict ISO regex +isNaNguard → 400 на невалидные датыtoDate < fromDate→ 400 (single-day range разрешён)MAX_RANGE_DAYS = 730guard (был: negative bypass приto < from)Specs
specs/roadmap-ux-backlog.feature— Gherkin BDD, 30 сценариев по 10 областямspecs/roadmap-ux-backlog-sdd.md— SDD, 8 разделов с TypeScript-сниппетами, state-таблицей, a11y-маппингом, edge cases и тест-планомПрошедшие ревью (перед PR)
Три параллельных ревью: code-reviewer, security-reviewer, UX/a11y reviewer — все HIGH и MEDIUM замечания устранены в коммите
44cdf1d.Устранённые HIGH:
touchClearRef— убрана утечкаtouchmove-listener при быстрых повторных тапахoutline:none+:focus-visibleCSS — надёжный focus-индикатор без imperative DOM-мутацииrole="button"+ вложенный<button>: chevron получилtabIndex={-1}+aria-hiddenrole="status"+aria-busy+aria-labelУстранённые MEDIUM (ключевые):
SAFE_COLOR_RE:\s*→[ \t]*— запрет newline в CSS-функцияхBarTooltipполучаетtodayпропом (устранена независимаяgetToday())e.metaKey || e.ctrlKey || e.altKeyconst t = getToday()один раз вместо 9 вызовов в циклеmilestoneYOffsetsзавёрнут вuseMemo([rows, dayPx])aria-haspopup="dialog"(не"true")prefers-reduced-motion: добавленtransition:none!importantaria-live="polite"span — анонс смены масштаба для screen readerrole="img"+aria-labelTest plan
dueDate→ ромб в позиции дедлайна (регрессия предыдущего PR)dueDate→ 3 ромба со смещением, не перекрываются?→ легенда; click outside → закрывается; Escape → закрываетсяdueDateдоrange.start→ красная полоска слева + tooltip при hoverprefers-reduced-motion: reduce→ нет анимаций и переходов🤖 Generated with Claude Code