diff --git a/docs/TZ.md b/docs/TZ.md index 35d1139..42eaa9a 100644 --- a/docs/TZ.md +++ b/docs/TZ.md @@ -4,7 +4,7 @@ | | | |---|---| -| Версия ТЗ | 0.85 | +| Версия ТЗ | 0.86 | | Дата | 2026-10-10 | | Статус | На обсуждении | @@ -1013,6 +1013,15 @@ открывалось затемнённым, а клик по пункту лишь закрывал его. Теперь `position: relative` и `z-index` включены только с 768px; на мобильных шапка контекста не создаёт, шторка попадает в корневой контекст и лежит выше затемнения. +- **Форма создания задачи на мобильных закрывается сама** (0.86): после успешного + создания задача форма закрывается — и в глобальной форме из «+» в топбаре, и в + форме новой задачи проекта, и в форме подзадачи. На десктопе она, наоборот, + остаётся открытой: там удобно набивать несколько задач подряд, не открывая форму + заново. Правило применяется только к **созданию**: при правке существующей задачи + форма закрывается всегда (после сохранения смотреть в ней больше нечего). + Порог мобильного — тот же 767px, что у шторки меню (0.77), и объявлен в одном месте + (`frontend/src/viewport.ts`), чтобы поведения на одной ширине не разошлись. + Быстрый захват строкой в стеке (3.1) не форма: он очищает поле и остаётся на месте. - **Меню у правого края экрана** (0.79): выравнивание координат (`dropdownClamp.ts`) ставило меню инлайновый `right: 0`, но китовый `.dropdown-menu{left: 0}` при этом продолжал действовать — оба края заданы, и при `min-width: 220px` меню оставалось diff --git a/frontend/src/App.vue b/frontend/src/App.vue index c4d2013..9a5807f 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -7,6 +7,7 @@ import { startRealtime } from './realtime' import { setStorageUserId } from './userScope' import { setCurrency } from './taskui' +import { isMobile } from './viewport' import GamifyBridge from './components/GamifyBridge.vue' import TaskForm from './components/TaskForm.vue' import type { UserInfo } from './types' @@ -70,7 +71,10 @@ }) function onCreated(task: Task) { - // форму не закрываем — массовое добавление; страница уходит к созданной + // На десктопе форму не закрываем — массовое добавление; страница уходит к созданной. + // На мобильных наоборот: экран занят целиком, следующая задача подряд — редкий + // случай, и оставленная форма перекрывала бы то, что только что создано (ТЗ 0.86) + if (isMobile()) createOpen.value = false void router.push(`/tasks/${task.id}`) } diff --git a/frontend/src/dropdownClamp.ts b/frontend/src/dropdownClamp.ts index 3e0c0eb..3dad3aa 100644 --- a/frontend/src/dropdownClamp.ts +++ b/frontend/src/dropdownClamp.ts @@ -2,14 +2,14 @@ // (position:absolute; left:0), и у триггеров у правого края экрана меню вылезает // наружу. Кит не даёт ручек позиционирования — после открытия выравниваем меню // (и по горизонтали, и по вертикали) в пределах вьюпорта, не трогая стили кита. -// +import { MOBILE_QUERY } from './viewport' + // На мобильных (до 767px, ТЗ 0.77) этого не нужно: меню целиком раскладывается // как шторка снизу из kit-overrides.css, и инлайн-координаты только мешали бы. -const MOBILE = '(max-width: 767px)' export function installDropdownClamp(): void { const clamp = () => { - const mobile = window.matchMedia(MOBILE).matches + const mobile = window.matchMedia(MOBILE_QUERY).matches for (const dropdown of Array.from(document.querySelectorAll('.dropdown.is-open'))) { const menu = dropdown.querySelector('.dropdown-menu') if (!menu) continue diff --git a/frontend/src/viewport.ts b/frontend/src/viewport.ts new file mode 100644 index 0000000..f6baee8 --- /dev/null +++ b/frontend/src/viewport.ts @@ -0,0 +1,9 @@ +// Мобильный экран: тот же порог, что у шторки выпадающих меню (ТЗ 0.77). +// Живёт одним объявлением, потому что от него зависит поведение разных виджетов +// (шторка меню, закрытие формы после создания задачи) — разошедшиеся пороги дали бы +// интерфейс, который на одной и той же ширине ведёт себя по-разному. +export const MOBILE_QUERY = '(max-width: 767px)' + +export function isMobile(): boolean { + return window.matchMedia(MOBILE_QUERY).matches +} diff --git a/frontend/src/views/ProjectView.vue b/frontend/src/views/ProjectView.vue index 564d0b2..e243087 100644 --- a/frontend/src/views/ProjectView.vue +++ b/frontend/src/views/ProjectView.vue @@ -8,6 +8,7 @@ import { useRealtimeRefresh } from '../realtime' import { scopedKey } from '../userScope' import { deadlineOverdue, gradeToPriority, priorityOptions, priorityToGrade, priorityLabel, priorityVariant, relevanceLabel, renderMarkdown, setPageTitle, statusIcon, statusOptions } from '../taskui' +import { isMobile } from '../viewport' import PriorityBadge from '../components/PriorityBadge.vue' import MdEditor from '../components/MdEditor.vue' import MdLightbox from '../components/MdLightbox.vue' @@ -688,6 +689,8 @@ // Массовое добавление: форму не закрываем (проект остаётся дефолтом); // переход к задаче не делаем — drawer умрёт вместе со страницей function onTaskCreated() { + // На мобильных форма создания закрывается (ТЗ 0.86), на десктопе остаётся + if (isMobile()) newTaskOpen.value = false void load() } diff --git a/frontend/src/views/StackView.vue b/frontend/src/views/StackView.vue index 1017fdc..655d7e0 100644 --- a/frontend/src/views/StackView.vue +++ b/frontend/src/views/StackView.vue @@ -6,6 +6,7 @@ import { intlLocale } from '../i18n/locale' import { useRealtimeRefresh } from '../realtime' import { proposalItems } from '../taskui' +import { isMobile } from '../viewport' import { useToast } from 'gnexus-ui-kit/vue' import TaskForm from '../components/TaskForm.vue' import Dashboard from '../components/Dashboard.vue' @@ -113,6 +114,9 @@ // Массовое добавление: форму не закрываем; переход к задаче не делаем — // drawer умрёт вместе со страницей. Новая «сырая» задача видна в стеке за формой. function onCreated() { + // Форма создания на мобильных закрывается после первой задачи (ТЗ 0.86), + // на десктопе остаётся открытой ради массового ввода + if (isMobile()) createOpen.value = false void loadAll() } diff --git a/frontend/src/views/TaskView.vue b/frontend/src/views/TaskView.vue index 23c2b1a..b9368c4 100644 --- a/frontend/src/views/TaskView.vue +++ b/frontend/src/views/TaskView.vue @@ -27,6 +27,7 @@ import { useToast } from 'gnexus-ui-kit/vue' import { celebrateEarned } from '../gamification' import { onRealtime } from '../realtime' +import { isMobile } from '../viewport' import InlineField from '../components/InlineField.vue' import ConfirmDialog from '../components/ConfirmDialog.vue' import TagField from '../components/TagField.vue' @@ -616,7 +617,9 @@ const subtaskOpen = ref(false) function onSubtaskSaved() { - // форма остаётся открытой — массовое добавление подзадач; страница обновится + // форма остаётся открытой — массовое добавление подзадач; страница обновится. + // На мобильных закрываем: там экран занят целиком (ТЗ 0.86) + if (isMobile()) subtaskOpen.value = false void load() }