Skip to content

feat(tasks): виджет «Идёт сейчас» в сайдбаре - #22

Merged
MegaBadCoder merged 6 commits into
mainfrom
feat/sidebar-current-task-widget
Aug 13, 2026
Merged

feat(tasks): виджет «Идёт сейчас» в сайдбаре#22
MegaBadCoder merged 6 commits into
mainfrom
feat/sidebar-current-task-widget

Conversation

@MegaBadCoder

@MegaBadCoderMegaBadCoder commented Aug 13, 2026

Copy link
Copy Markdown
Owner

В сайдбаре секции задач, над списком проектов, появился блок «Идёт сейчас»: задачи, чьё временное окно содержит текущий момент, с остатком времени, прогрессом и запуском помодоро. Когда активных задач нет — блок исчезает целиком, чтобы не занимать место бо́льшую часть суток.

ИДЁТ СЕЙЧАС
▌ Рабочая сессия ● идёт ← таймер тикает по этой задаче
▌ осталось 3 ч 2 мин · 0/4
▌ ━━━━╸────────────────
▌ Математика ▷ ← можно запустить
▌ осталось 45 мин · 0/2
▌ ━━━╸─────────────────
ПРОЕКТЫ +
▢ Учёба

Что показывает строка

Остаток времени, долю пройденного окна и счётчик помидоров. Виджет отвечает не только на «чем я занят», но и на «сколько ещё» — иначе за этим всё равно пришлось бы идти в календарь. Левая полоса — эхо линии текущего времени в календаре, чтобы блок не читался как ещё один список.

Окно задачи: dueDatedueDate + computeTaskDurationMinutes(task). Граница конца исключающая, иначе две смежные задачи показывались бы вдвоём в момент стыка. Из выборки выпадают задачи без dueDate, all-day (время ровно 00:00, как в calendar-events.ts), выполненные и isOverdue.

Состояние таймера в строке

СостояниеЧто видноКнопка
тикает по этой задачекрасная пульсирующая «идёт»нет
взведён, но не тикаетприглушённая «пауза»продолжает через toggleTimer
не запущен или идёт по другой задачезапускает через startTask

Кнопка убрана в «идёт» не ради вида: startTask начинает сессию с первой фазы, поэтому случайный клик по идущей задаче стирал бы уже отработанные помидоры. Такая возможность существовала до этого PR.

При этом в «паузе» кнопка обязана остаться. startTask только взводит сессию — тикать она начинает отдельно, так же ведут себя обе существующие кнопки в приложении. Значит сразу после клика по кнопке виджета строка попадает именно в «паузу», и если бы кнопка там пропадала, запустить таймер из виджета стало бы невозможно.

Для этого timer-store начал отдавать activeTaskId: без него строка не отличает «идёт» от «идёт по другой задаче».

Решения, которые стоит отметить на ревью

Виджет читает taskStore.tasks напрямую, а не через tasksToCalendarEvents. Последний отдаёт ещё и виртуальные инстансы повторяющихся задач с id вида id__virtual__ts, которых нет на бэке — первый же инкремент помидора по такой задаче упал бы с 404. Отсекать на источнике данных надёжнее, чем не забывать проверять флаг isVirtual.

sectionExtraRegistry держит один компонент на секцию, поэтому виджет и дерево проектов скомпонованы в TasksSidebarSection.vue, а не через расширение реестра до массива — у него ровно один потребитель. Обёртка попутно закрывает оба слота section-extra разом, так что виджет работает и в мобильном сайдбаре без отдельной проводки. defineAsyncComponent переехал на обёртку — сайдбар остался одним ленивым чанком.

Вся логика — в чистых функциях, компоненты тонкие. now передаётся аргументом, а не читается из часов, поэтому правила тестируются без подмены таймеров. Окно считается в одном приватном getWindow(), на котором построены и фильтр активных задач, и расчёт прогресса — разойтись они не могут.

Попутный рефакторинг

Маппинг задачи в аргумент startTask был скопирован в TasksView и TaskDetailDialog, оба раза с инлайновыми 25/5/15/4 вместо лежащего рядом POMODORO_DEFAULTS. Виджет стал бы третьей копией — вместо этого вынесен toTimerTask, оба места переведены на него.

Там намеренно оставлен ||, а не ??: исходные вызовы схлопывали нулевую длительность в дефолт, и тихая замена оператора запускала бы помидор нулевой длины. Тест на pomodoroDuration: 0 → 25 написан до перевода вызовов, чтобы поймать такую подмену.

Проверено

  • 354 юнит-теста (на main было 312), vue-tsc чисто, npm run build проходит
  • живой стенд в браузере: обе темы, полный цикл запуска таймера из виджета, мобильная панель, длинное название обрезается многоточием без горизонтального скролла
  • отдельный тест на прокрутку часов: задача уходит из виджета, когда её окно истекает

В светлой теме строки изначально были почти невидимы — bg-sidebar-accent/40 давал oklch(0.968) против фона сайдбара oklch(0.985), разница меньше двух процентов. Замерено в браузере, добавлен бордер. Вторичный текст проверен отдельно: контраст ≈5:1.

Кнопка визуально 32px, но тач-таргет расширен до 44×44 через ::before, чтобы не разрежать плотный сайдбар. Прогресс-бар и пульсация уважают prefers-reduced-motion, прогресс скрыт от скринридеров — текст рядом говорит то же самое.

Осознанно не сделано

  • Клика по строке нет: виджет показывает и запускает таймер, не более.
  • Минутный тик даёт задержку до 59 секунд — ровно столько же у красной линии календаря, рассогласования не будет.
  • В зазоре между смежными задачами виджет исчезает и список проектов подпрыгивает вверх.
  • Повторяющаяся задача с просроченным dueDate, для которой календарь рисует призрак на сегодня, в виджет не попадёт — следствие решения брать только реальные задачи.
  • HourGrid продолжает держать свой setInterval вместо нового useNow: инвариант задачи запрещал трогать календарь.
  • computeTaskDurationMinutes берёт дефолты через ??, а toTimerTask — через ||. Для задачи с pomodoroCount: 0 это расходится, но на достижимых путях не проявляется: такая задача не попадает в виджет вовсе. Файл пре-существует и задачей не затрагивался.

Дизайн, план, результаты проверки и разбор — в docs/tasks/sidebar-current-task-widget.md.

i.seliverstov added 6 commits August 13, 2026 13:04
getActiveTasksAt answers "which tasks does the calendar's red line cross right
now". `now` is an argument rather than read from the clock, so the rule is pure
and testable without faking timers. The end bound is exclusive so two
back-to-back tasks never appear together.
toTimerTask centralises the task -> startTask mapping that was copy-pasted in
TasksView and TaskDetailDialog. It keeps `||` rather than `??` on purpose: the
call sites it replaces collapse a stored 0 into the default, and a silent
switch to `??` would start a zero-length pomodoro. A test pins that.
Both call sites built the startTask argument by hand with inline 25/5/15/4
literals. They now share the helper, so the defaults live in one place and a
third copy cannot drift.
Behaviour is unchanged — the helper keeps the same `||` coercion.
Shows the tasks whose scheduled window contains now, with a start button on
the pomodoro ones. Hidden entirely when nothing is running, so the sidebar
stays quiet for most of the day.
sectionExtraRegistry holds one component per section, so the widget and the
project tree are composed in a TasksSidebarSection wrapper rather than by
teaching the registry about lists — it has exactly one consumer. The wrapper
also lands in both section-extra slots at once, so the widget shows up in the
mobile sidebar without extra wiring. defineAsyncComponent moves up to the
wrapper, keeping the sidebar a single lazy chunk.
The widget reads taskStore.tasks directly rather than going through
tasksToCalendarEvents: the latter also yields virtual recurring instances,
whose `__virtual__` ids do not exist on the backend and would 404 on the first
pomodoro increment.
@MegaBadCoder
MegaBadCoder merged commit ba8243b into mainAug 13, 2026
3 checks passed
@MegaBadCoderMegaBadCoder changed the title feat(tasks): виджет «Текущая задача» в сайдбареfeat(tasks): виджет «Идёт сейчас» в сайдбареAug 13, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@MegaBadCoder