diff --git a/docs/TZ.md b/docs/TZ.md index 03a4088..b3c1a31 100644 --- a/docs/TZ.md +++ b/docs/TZ.md @@ -4,7 +4,7 @@ | | | |---|---| -| Версия ТЗ | 0.82 | +| Версия ТЗ | 0.83 | | Дата | 2026-10-10 | | Статус | На обсуждении | @@ -231,7 +231,8 @@ (слот `footer` китового `GnCard`), а не в ряду бейджей: подвал отделён чертой 1px и прижат к низу карточки, поэтому в сетке списка он у всех карточек на одной линии. Иконка рядом с текстом идёт с китовым классом `normalize` — без него глиф - иконочного шрифта садится на 1.7px выше середины строки. + иконочного шрифта садится на 1.7px выше середины строки (в подвале карточки сама + поправка снята с 0.83 — см. 3.19). - **Проект задачи — из меню карточки** (0.82): в «⋯»-меню карточки задачи рядом стоят «Перейти к проекту» и «Переместить в проект» — это один и тот же вопрос «в каком проекте задача». «Перейти» показывается у задачи с проектом и **не показывается на @@ -243,7 +244,9 @@ бэкенд проверяет, что проект принадлежит пользователю, и рассылает `task.changed`. **XP и монеты за перенос не начисляются** — они висят только на переходе в «завершено». Успех подтверждается тостом, список перечитывается сразу (`moved`), - поэтому задача тут же уходит со страницы прежнего проекта. + поэтому задача тут же уходит со страницы прежнего проекта. В 0.83 пункт сокращён до + **«В другой проект»**: надпись «Переместить в проект» в китовом меню шириной 220px + рвалась на две строки. ### 3.7. Главная страница (дашборд + стек) @@ -438,6 +441,32 @@ работает на значениях по умолчанию (по статусу — «К выполнению»). Сохранённый прежними версиями выбор тега по `id` не восстанавливается осознанно: имя тега по `id` известно только после загрузки справочника. +- **Теплокарта активности проекта** (0.83): на странице проекта, в сайдбаре рядом с + «Статистикой», — та же сетка «как в гитхабе», что на странице статистики, но по + задачам одного проекта: столбец — неделя (с понедельника), строка — день недели, + заливка — по числу **закрытых** за день задач (шкала общая со статистикой: + 1 / 2–3 / 4–6 / 7+). Дни без закрытий — пустые клетки, будущие дни — почти + погашенные. В подсказке клетки — дата и оба числа: «закрыто N, создано M» + (заливка про закрытия — это и есть работа по проекту, появления видны в + подсказке). Периодов два, переключатель — табами в карточке: **«За месяц» + (по умолчанию)** — пять недель, 35 дней, и «За полгода» — 26 недель, 182 дня. + Бэкенд для неё не нужен: страница проекта и так держит все свои задачи, включая + завершённые (3.6), поэтому история считается на клиенте — закрытия по `done_at`, + появления по `created_at`. В содержимое сайдбара сетка влезает без горизонтальной + прокрутки: в полугодовом режиме ячейки 9px с зазором 2px (26 недель — 284px), + в месячном — 18px (пятью неделями мелкая сетка терялась бы в широкой карточке); + легенда шкалы встаёт рядом с сеткой, а когда сетка занимает всю ширину — + переносится под неё. +- **Теги проекта — облаком, клик по тегу фильтрует** (0.83): список тегов в карточке + «Теги» заменён облаком — теги идут подряд, а размер шрифта показывает, сколько + задач с тегом (0.85–1.2em по ранжиру, точное число — в подсказке). Клик по тегу + ставит его в фильтр (тот же поиск по подстроке имени, 0.70), клик по активному тегу + фильтр снимает; активный тег подсвечен акцентом. До 960px сайдбар стоит ниже списка + задач, поэтому после клика список подтягивается к верху экрана — иначе результат + фильтра оказывался бы за кадром. +- **Задачи проекта — по две в ряд** (0.83): карточки задач на странице проекта + раскладываются сеткой по две в ряд (до 700px — одна колонка, как и раньше). + Карточки одного ряда выравниваются по высоте, подвал прижат к низу. #### 3.8.1. Архив @@ -997,6 +1026,24 @@ 8px сверху против 15px снизу, поэтому «2 недели назад» висело на 3.5px выше середины полосы. Отступы выровнены (12px сверху и снизу), полоса от этого выше прежней на пиксель. +- **Иконка в подвале карточки: китовая поправка снята** (0.83): китовое + `.ph.normalize` (`position:relative; top:.15em`) рассчитано на иконку **на общей + базовой линии** с текстом — там оно опускает глиф из 0.5em над базовой линией + (середина ink иконочного шрифта) до середины прописных, 0.36em, и это верно. Но + строка подвала — флекс с `align-items:center`: флекс-элемент коробкой в базовой + линии не участвует, а у иконочного шрифта нет нижнего выноса + (`fontBoundingBoxDescent = 0`), поэтому коробка иконки центрируется в строке и её + низ ложится на 0.125em **ниже** базовой линии текста. Поправка кита складывалась с + этим центрированием, и глиф уезжал вниз на 0.135em ≈ 1.6px: низ глифа уходил под + базовую линию, иконка читалась провалившейся. Замер по пикселям на одной и той же + строке («2 недели назад»): центр глифа на 1.3px ниже центра цифры «2». В подвале + карточки (свежесть, «без активности», отсчёт срока) поправка снята — `top: 0` в + `kit-overrides.css`: расчёт даёт 0.375em против 0.36em, то есть 0.2px, и низ глифа + садится на базовую линию, как у буквы. Класс `normalize` в разметке остаётся — + у кита он один на все иконки в тексте, а правка кита заказана отдельной задачей + в проекте GNtodo (иконке в флекс-строке поправка не нужна вовсе: предлагается + `vertical-align: -0.15em` вместо `position:relative; top`, либо отдельный класс + без поправки). - **Иконка в подвале карточки проекта выровнена по тексту** (0.81): у свежести и у отсчёта срока иконка шла без китового класса `normalize`, глиф иконочного шрифта садился выше середины строки и читался подвешенным — как в карточке задачи, где diff --git a/frontend/src/components/ProjectHeatmap.vue b/frontend/src/components/ProjectHeatmap.vue new file mode 100644 index 0000000..c76dcae --- /dev/null +++ b/frontend/src/components/ProjectHeatmap.vue @@ -0,0 +1,219 @@ + + + + + diff --git a/frontend/src/components/TaskCard.vue b/frontend/src/components/TaskCard.vue index 559c42d..20fb780 100644 --- a/frontend/src/components/TaskCard.vue +++ b/frontend/src/components/TaskCard.vue @@ -70,7 +70,9 @@ ] : []), { - label: t('task.moveToProject'), + // «В другой проект» (0.83) вместо «Переместить в проект»: в китовом меню + // (220px) длинный пункт рвался на две строки + label: t('task.moveToOther'), icon: 'ph-arrows-left-right', onSelect: () => { moveOpen.value = true diff --git a/frontend/src/kit-overrides.css b/frontend/src/kit-overrides.css index 6fb1b3a..dfbb9fa 100644 --- a/frontend/src/kit-overrides.css +++ b/frontend/src/kit-overrides.css @@ -133,6 +133,20 @@ color: var(--gn-color-text-medium, #a9b1d6); font-size: 0.85em; } +/* Иконка в строке подвала (0.83). Китовое `.ph.normalize` (top:.15em) рассчитано на + общую базовую линию: оно опускает глиф из 0.5em над базовой линией до середины + прописных. Но строка подвала («2 недели назад») — флекс с align-items:center, и + коробка иконки центрируется сама; у иконочного шрифта нет нижнего выноса + (fontBoundingBoxDescent = 0), поэтому низ коробки ложится на 0.125em ниже базовой + линии текста. Поправка складывается с этим центрированием, и глиф уезжает вниз на + 0.135em ≈ 1.6px — низ глифа уходит под базовую линию, и иконка читается провалившейся + (замер по пикселям на одной и той же строке: центр глифа на 1.3px ниже центра цифры + «2», рядом с которой он и стоит). Без поправки низ глифа Phosphor садится ровно на + базовую линию, как у буквы: расчёт даёт 0.375em против 0.36em середины прописных, то + есть 0.2px — иконке в текстовой строке больше и не нужно */ +.card-footer .ph.normalize { + top: 0; +} /* --- Дропдауны на мобильных: шторка снизу (0.77) --- */ /* Кит позиционирует меню от левого края триггера (position:absolute; left:0, diff --git a/frontend/src/locales/en.ts b/frontend/src/locales/en.ts index 9536edf..285671b 100644 --- a/frontend/src/locales/en.ts +++ b/frontend/src/locales/en.ts @@ -366,6 +366,9 @@ // в другой — соседние пункты, это один вопрос «в каком проекте задача» goToProject: 'Go to project', moveToProject: 'Move to project', + // Card menu item (0.83): "Move to project" wrapped onto two lines in the kit's + // 220px menu, and inside the menu the question is "which project" + moveToOther: 'To another project', moveHint: 'Task "{title}" will move to the selected project', moved: 'Task moved to "{project}"', movedOut: 'Task taken out of its project', @@ -397,6 +400,16 @@ overdue: 'Overdue', taskCount: '{n} tasks', openSite: 'Open site', + // Project activity (0.83): the same GitHub-style grid as on the stats page, + // but over one project's tasks and with short ranges — the card sits in the sidebar + activity: 'Activity', + heatMonth: 'A month', + heatHalf: 'Half a year', + heatCell: '{date} — closed {done}, created {created}', + // Tag cloud (0.83): a click sets a substring filter by the tag name + tagShow: 'Show tasks tagged "{name}"', + tagHide: 'Clear the "{name}" tag filter', + tagCount: 'Tasks with this tag: {n}', }, list: { title: 'Task list', diff --git a/frontend/src/locales/ru.ts b/frontend/src/locales/ru.ts index db07780..5e5bd04 100644 --- a/frontend/src/locales/ru.ts +++ b/frontend/src/locales/ru.ts @@ -366,6 +366,9 @@ // в другой — соседние пункты, это один вопрос «в каком проекте задача» goToProject: 'Перейти к проекту', moveToProject: 'Переместить в проект', + // Пункт меню карточки (0.83): «Переместить в проект» в китовом меню (220px) + // рвалось на две строки, а в самом меню вопрос звучит как «в какой проект» + moveToOther: 'В другой проект', moveHint: 'Задача «{title}» уедет в выбранный проект', moved: 'Задача перенесена в «{project}»', movedOut: 'Задача вынута из проекта', @@ -398,6 +401,16 @@ overdue: 'Просрочено', taskCount: '{n} задач', openSite: 'Открыть сайт', + // Активность проекта (0.83): та же сетка «как в GitHub», что в статистике, + // но по задачам одного проекта и с короткими периодами — карточка в сайдбаре + activity: 'Активность', + heatMonth: 'За месяц', + heatHalf: 'За полгода', + heatCell: '{date} — закрыто {done}, создано {created}', + // Облако тегов (0.83): клик по тегу ставит подстрочный фильтр по его имени + tagShow: 'Показать задачи с тегом «{name}»', + tagHide: 'Снять фильтр по тегу «{name}»', + tagCount: 'Задач с этим тегом: {n}', }, list: { title: 'Список задач', diff --git a/frontend/src/locales/uk.ts b/frontend/src/locales/uk.ts index b7ac8ca..cb01eab 100644 --- a/frontend/src/locales/uk.ts +++ b/frontend/src/locales/uk.ts @@ -366,6 +366,9 @@ // в інший — сусідні пункти, це одне питання «у якому проєкті задача» goToProject: 'Перейти до проєкту', moveToProject: 'Перемістити до проєкту', + // Пункт меню картки (0.83): «Перемістити до проєкту» в китовому меню (220px) + // рвалося на два рядки, а в меню питання звучить як «до якого проєкту» + moveToOther: 'До іншого проєкту', moveHint: 'Завдання «{title}» переїде до вибраного проєкту', moved: 'Завдання перенесено до «{project}»', movedOut: 'Завдання вилучено з проєкту', @@ -397,6 +400,16 @@ overdue: 'Прострочено', taskCount: '{n} задач', openSite: 'Відкрити сайт', + // Активність проєкту (0.83): та сама сітка «як у GitHub», що в статистиці, + // але за завданнями одного проєкту й з короткими періодами — картка в сайдбарі + activity: 'Активність', + heatMonth: 'За місяць', + heatHalf: 'За півроку', + heatCell: '{date} — закрито {done}, створено {created}', + // Хмара теґів (0.83): клік по теґу ставить підрядковий фільтр за його іменем + tagShow: 'Показати завдання з теґом «{name}»', + tagHide: 'Зняти фільтр за теґом «{name}»', + tagCount: 'Завдань із цим теґом: {n}', }, list: { title: 'Список задач', diff --git a/frontend/src/views/ProjectView.vue b/frontend/src/views/ProjectView.vue index 6611db0..564d0b2 100644 --- a/frontend/src/views/ProjectView.vue +++ b/frontend/src/views/ProjectView.vue @@ -18,6 +18,7 @@ import ConfirmDialog from '../components/ConfirmDialog.vue' import TaskForm from '../components/TaskForm.vue' import TaskCard from '../components/TaskCard.vue' +import ProjectHeatmap from '../components/ProjectHeatmap.vue' import Pagination from '../components/Pagination.vue' defineOptions({ name: 'ProjectView' }) @@ -236,6 +237,34 @@ return [...counts.entries()].sort((a, b) => b[1] - a[1]) }) +// Облако тегов (0.83) вместо списка: клик по тегу ставит фильтр. Размер имени и есть +// счётчик — чем чаще тег в задачах проекта, тем крупнее (0.85…1.2em); точное число +// осталось в подсказке, чтобы облако не превращалось в таблицу +function tagFontSize(count: number): string { + const max = tagRows.value[0]?.[1] ?? 1 + const min = tagRows.value[tagRows.value.length - 1]?.[1] ?? 1 + if (max === min) return '1em' + return `${(0.85 + 0.35 * ((count - min) / (max - min))).toFixed(3)}em` +} + +// Активным считаем тег, чьё имя стоит в поиске: фильтр подстрочный, поэтому +// «bug» в поле подсвечивает и тег «bugfix», если он совпал целиком +function isActiveTag(name: string): boolean { + return tagQuery.value.toLowerCase() === name.toLowerCase() +} + +// Список задач на узком экране лежит выше сайдбара — после клика по тегу уводим +// к нему, иначе результат фильтрации остался бы за кадром +const listAnchor = ref(null) + +function filterByTag(name: string) { + // повторный клик по активному тегу снимает фильтр + filters.value.tag = isActiveTag(name) ? '' : name + if (window.matchMedia('(max-width: 960px)').matches) { + void nextTick(() => listAnchor.value?.scrollIntoView({ behavior: 'smooth', block: 'start' })) + } +} + const noteHtml = computed(() => (project.value ? renderMarkdown(project.value.note) : '')) // README репозитория проекта (0.67) — второе описание. Раньше заметка из одной @@ -955,7 +984,7 @@ :aria-label="t('list.filterStatus')" @update:model-value="filters.status = $event" /> -
+
+ + -
-
-