[bug][ui][temporary-notes] Баннер временной заметки разваливается на мобильном: текст лесенкой по одному слову и уезжает под кнопку «Move to trash» #321

Closed
opened 2026-07-03 23:13:48 +03:00 by agent_vscode · 0 comments
Collaborator

Симптом

На узком экране (телефон, ~390px) баннер временной заметки превращается в кашу: текст сжат в колонку уже самого длинного слова и рассыпается «лесенкой» по одному слову на строку, а переполняющие слова уезжают вправо под прозрачную кнопку «Move to trash» (variant="subtle") — текст и кнопка визуально накладываются. «Make permanent» прижата к правому краю. На десктопе всё выглядит нормально.

(скрин: мобильный Safari, Edit-режим, страница Untitled с баннером «This temporary note moves to trash in 10 hours…»)

Репро

  1. Создать временную заметку под пользователем с правом Edit в спейсе (без Edit кнопки не рендерятся — canEdit — и баннер выглядит нормально; поэтому ловят именно редакторы).
  2. Открыть её на телефоне или сузить окно браузера до ~400–500px.
  3. Баннер: текст в столбик, «Move to trash» лежит поверх текста.

Корень

apps/client/src/features/page/components/temporary-note-banner.tsx:72-113. Два флекс-эффекта складываются:

  1. Перенос не срабатывает никогда. Внешняя Group justify="space-between" wrap="wrap", но текстовая группа имеет style={{ flex: 1, minWidth: 0 }}flex: 1 1 0%, т.е. flex-basis = 0. Базовый размер флекс-строки — это только кнопочная группа (~250px en), она «влезает» всегда → wrap="wrap" ни при какой ширине не уводит кнопки на вторую строку; вместо этого ужимается текст.
  2. Кнопки не сжимаются — переполняется текст. Кнопочная Group wrap="nowrap" имеет min-width: auto ≈ min-content ≈ 250px (en) и не сжимается. Тексту на телефоне остаётся ~330 − 250 − gap ≈ 70px — меньше самого длинного слова («permanent.» ≈ 80px при size="sm"). Слова посимвольно не переносятся, и Text переполняет свою колонку (это разрешил minWidth: 0) вправо — прямо под subtle-кнопку с прозрачным фоном. Отсюда и наложение, и лесенка.

В ru-локали ещё хуже: «Переместить в корзину» + «Сделать постоянной» — nowrap-ряд ~350px+, который на 390px-экране не влезает даже в отдельную строку.

DeletedPageBanner, поведение которого этот баннер сознательно зеркалит, ровно эту проблему уже решает: apps/client/src/features/page/trash/components/deleted-page-banner.tsx:80-134 — на < sm полные кнопки заменяются на icon-only ActionIcon + Tooltip (visibleFrom="sm" / hiddenFrom="sm"). Когда в баннер временной заметки добавляли вторую кнопку (#273/#277), адаптивная часть паттерна не переехала.

Ожидаемое поведение

На мобильном баннер остаётся компактным: текст читается с нормальными переносами по всей ширине, оба действия доступны и ни на что не накладываются. Поведение согласовано с DeletedPageBanner.

Фикс (точечный)

Две части, обе в одном файле:

1. Скопировать адаптивный паттерн из DeletedPageBanner: на >= sm — текущие кнопки с подписями, на < sm — icon-only ActionIcon + Tooltip + aria-label. Это решает и ru-локаль (длинные подписи на мобильном исчезают вовсе).

2. Прививка для средних контейнеров (десктоп с открытым aside: viewport ≥ sm, а контейнер узкий — visibleFrom смотрит на viewport, не на контейнер): текстовой группе ненулевой flex-basis, чтобы внешний wrap="wrap" реально срабатывал и кнопки уходили на вторую строку вместо выдавливания текста.

// temporary-note-banner.tsx — layout-only change, handlers untouched
<Group justify="space-between" wrap="wrap" gap="sm">
  {/* Non-zero flex-basis so the outer wrap actually engages when the
      container is narrow: buttons drop to their own row instead of
      squeezing the text below its longest word (flex: 1 = basis 0
      never triggers wrapping). */}
  <Group gap="xs" wrap="nowrap" style={{ flex: "1 1 16rem", minWidth: 0 }}>
    <IconClockHour4 /* unchanged */ />
    <Text size="sm">{/* unchanged */}</Text>
  </Group>
  {canEdit && (
    <>
      {/* Full labeled buttons on >= sm — unchanged markup */}
      <Group gap="xs" wrap="nowrap" visibleFrom="sm">
        <Button /* Move to trash — unchanged */ />
        <Button /* Make permanent — unchanged */ />
      </Group>
      {/* Icon-only actions below sm — mirrors DeletedPageBanner */}
      <Group gap="xs" wrap="nowrap" hiddenFrom="sm">
        <Tooltip label={t("Move to trash")} withArrow>
          <ActionIcon
            size="lg"
            variant="subtle"
            color="red"
            onClick={handleTrashNow}
            loading={isDeleting}
            aria-label={t("Move to trash")}
          >
            <IconTrash size={18} />
          </ActionIcon>
        </Tooltip>
        <Tooltip label={t("Make permanent")} withArrow>
          <ActionIcon
            size="lg"
            variant="light"
            color="orange"
            onClick={handleMakePermanent}
            loading={toggleTemporary.isPending}
            aria-label={t("Make permanent")}
          >
            <IconClockHour4 size={18} />
          </ActionIcon>
        </Tooltip>
      </Group>
    </>
  )}
</Group>

16rem (256px): с en-кнопками (~250px) перенос на вторую строку включается при ширине контента примерно < 520px — телефоны и узкие сплиты попадают всегда, широкий десктоп не затрагивается.

Альтернатива (вариант Б, не основной): только flex-basis + ml="auto" у кнопок, без icon-only ряда — сохраняет подписи на мобильном, но баннер растёт до двух строк, в ru-локали рискует третьей и расходится с уже принятым паттерном DeletedPageBanner. Имеет смысл, только если icon-only «Make permanent» посчитаем недостаточно заметным для главного rescue-действия (тултип + aria-label этот риск смягчают — как и у «Restore page» в trash-баннере).

Затронутые файлы

  • apps/client/src/features/page/components/temporary-note-banner.tsx — единственная правка: разметка + импорты ActionIcon, Tooltip из @mantine/core. Handlers, логика и i18n-ключи не меняются (оба ключа уже в локалях как подписи кнопок).
  • Опционально, за компанию отдельным коммитом: та же flex-basis прививка в deleted-page-banner.tsx:71 — там латентно тот же нюанс с flex: 1, просто icon-only ряд узкий и до переполнения обычно не доходит.

Связанное

  • #201 — фича временных заметок: баннер появился там с одной кнопкой «Make permanent», ширины хватало.
  • #273 / #277 — добавили в баннер вторую кнопку «Move to trash»; она и создала дефицит ширины на мобильном, а адаптивный паттерн из DeletedPageBanner при этом не был скопирован.
## Симптом На узком экране (телефон, ~390px) баннер временной заметки превращается в кашу: текст сжат в колонку уже самого длинного слова и рассыпается «лесенкой» по одному слову на строку, а переполняющие слова уезжают вправо **под** прозрачную кнопку «Move to trash» (`variant="subtle"`) — текст и кнопка визуально накладываются. «Make permanent» прижата к правому краю. На десктопе всё выглядит нормально. (скрин: мобильный Safari, Edit-режим, страница Untitled с баннером «This temporary note moves to trash in 10 hours…») ## Репро 1. Создать временную заметку под пользователем с правом Edit в спейсе (без Edit кнопки не рендерятся — `canEdit` — и баннер выглядит нормально; поэтому ловят именно редакторы). 2. Открыть её на телефоне или сузить окно браузера до ~400–500px. 3. Баннер: текст в столбик, «Move to trash» лежит поверх текста. ## Корень `apps/client/src/features/page/components/temporary-note-banner.tsx:72-113`. Два флекс-эффекта складываются: 1. **Перенос не срабатывает никогда.** Внешняя `Group justify="space-between" wrap="wrap"`, но текстовая группа имеет `style={{ flex: 1, minWidth: 0 }}` → `flex: 1 1 0%`, т.е. **flex-basis = 0**. Базовый размер флекс-строки — это только кнопочная группа (~250px en), она «влезает» всегда → `wrap="wrap"` ни при какой ширине не уводит кнопки на вторую строку; вместо этого ужимается текст. 2. **Кнопки не сжимаются — переполняется текст.** Кнопочная `Group wrap="nowrap"` имеет `min-width: auto` ≈ min-content ≈ 250px (en) и не сжимается. Тексту на телефоне остаётся ~330 − 250 − gap ≈ 70px — меньше самого длинного слова («permanent.» ≈ 80px при `size="sm"`). Слова посимвольно не переносятся, и `Text` переполняет свою колонку (это разрешил `minWidth: 0`) вправо — прямо под subtle-кнопку с прозрачным фоном. Отсюда и наложение, и лесенка. В ru-локали ещё хуже: «Переместить в корзину» + «Сделать постоянной» — nowrap-ряд ~350px+, который на 390px-экране не влезает даже в отдельную строку. `DeletedPageBanner`, поведение которого этот баннер сознательно зеркалит, ровно эту проблему уже решает: `apps/client/src/features/page/trash/components/deleted-page-banner.tsx:80-134` — на `< sm` полные кнопки заменяются на icon-only `ActionIcon` + `Tooltip` (`visibleFrom="sm"` / `hiddenFrom="sm"`). Когда в баннер временной заметки добавляли вторую кнопку (#273/#277), адаптивная часть паттерна не переехала. ## Ожидаемое поведение На мобильном баннер остаётся компактным: текст читается с нормальными переносами по всей ширине, оба действия доступны и ни на что не накладываются. Поведение согласовано с `DeletedPageBanner`. ## Фикс (точечный) Две части, обе в одном файле: **1. Скопировать адаптивный паттерн из `DeletedPageBanner`:** на `>= sm` — текущие кнопки с подписями, на `< sm` — icon-only `ActionIcon` + `Tooltip` + `aria-label`. Это решает и ru-локаль (длинные подписи на мобильном исчезают вовсе). **2. Прививка для средних контейнеров** (десктоп с открытым aside: viewport ≥ sm, а контейнер узкий — `visibleFrom` смотрит на viewport, не на контейнер): текстовой группе ненулевой flex-basis, чтобы внешний `wrap="wrap"` реально срабатывал и кнопки уходили на вторую строку вместо выдавливания текста. ```tsx // temporary-note-banner.tsx — layout-only change, handlers untouched <Group justify="space-between" wrap="wrap" gap="sm"> {/* Non-zero flex-basis so the outer wrap actually engages when the container is narrow: buttons drop to their own row instead of squeezing the text below its longest word (flex: 1 = basis 0 never triggers wrapping). */} <Group gap="xs" wrap="nowrap" style={{ flex: "1 1 16rem", minWidth: 0 }}> <IconClockHour4 /* unchanged */ /> <Text size="sm">{/* unchanged */}</Text> </Group> {canEdit && ( <> {/* Full labeled buttons on >= sm — unchanged markup */} <Group gap="xs" wrap="nowrap" visibleFrom="sm"> <Button /* Move to trash — unchanged */ /> <Button /* Make permanent — unchanged */ /> </Group> {/* Icon-only actions below sm — mirrors DeletedPageBanner */} <Group gap="xs" wrap="nowrap" hiddenFrom="sm"> <Tooltip label={t("Move to trash")} withArrow> <ActionIcon size="lg" variant="subtle" color="red" onClick={handleTrashNow} loading={isDeleting} aria-label={t("Move to trash")} > <IconTrash size={18} /> </ActionIcon> </Tooltip> <Tooltip label={t("Make permanent")} withArrow> <ActionIcon size="lg" variant="light" color="orange" onClick={handleMakePermanent} loading={toggleTemporary.isPending} aria-label={t("Make permanent")} > <IconClockHour4 size={18} /> </ActionIcon> </Tooltip> </Group> </> )} </Group> ``` `16rem` (256px): с en-кнопками (~250px) перенос на вторую строку включается при ширине контента примерно < 520px — телефоны и узкие сплиты попадают всегда, широкий десктоп не затрагивается. **Альтернатива (вариант Б, не основной):** только flex-basis + `ml="auto"` у кнопок, без icon-only ряда — сохраняет подписи на мобильном, но баннер растёт до двух строк, в ru-локали рискует третьей и расходится с уже принятым паттерном `DeletedPageBanner`. Имеет смысл, только если icon-only «Make permanent» посчитаем недостаточно заметным для главного rescue-действия (тултип + aria-label этот риск смягчают — как и у «Restore page» в trash-баннере). ## Затронутые файлы - `apps/client/src/features/page/components/temporary-note-banner.tsx` — единственная правка: разметка + импорты `ActionIcon`, `Tooltip` из `@mantine/core`. Handlers, логика и i18n-ключи не меняются (оба ключа уже в локалях как подписи кнопок). - Опционально, за компанию отдельным коммитом: та же flex-basis прививка в `deleted-page-banner.tsx:71` — там латентно тот же нюанс с `flex: 1`, просто icon-only ряд узкий и до переполнения обычно не доходит. ## Связанное - #201 — фича временных заметок: баннер появился там с одной кнопкой «Make permanent», ширины хватало. - #273 / #277 — добавили в баннер вторую кнопку «Move to trash»; она и создала дефицит ширины на мобильном, а адаптивный паттерн из `DeletedPageBanner` при этом не был скопирован.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#321