setSelectedService(null)}
+ aria-label="Закрыть детали назначения" onClick={() => {setSelectedService(null);returnToMapControl()}}
className="text-muted-foreground hover:text-foreground transition-colors"
>
@@ -3206,7 +3231,7 @@ function NetworkMapContent() {
aria-label="Навести на карте" title="Навести на карте">
- { setSelected(null); setSelWanIdx(null) }}
+ { setSelected(null); setSelWanIdx(null); returnToMapControl() }}
className="text-muted-foreground hover:text-foreground transition-colors">
diff --git a/docs/ux-ui-plan.md b/docs/ux-ui-plan.md
index 782ace5d..ddf5b8fd 100644
--- a/docs/ux-ui-plan.md
+++ b/docs/ux-ui-plan.md
@@ -189,4 +189,4 @@ HTTP-фикстуры; локальная приёмка: 43 основных +
| Оболочка · [Frame](https://reui.io/components/frame?ref=mcp) / [API](https://reui.io/docs/components/base/frame?ref=mcp) | [Обзор](ux-ui/screens/network-map-overview-desktop.png) | [390 px](ux-ui/screens/network-map-overview-mobile.png), [320 px](ux-ui/screens/network-map-overview-narrow.png) | Источник → свежесть → слои/фильтры → карта |
| Детали · Frame / [List 9](https://reui.io/preview/base/list-9?ref=mcp) | [Детали](ux-ui/screens/network-map-detail-desktop.png) | [390 px](ux-ui/screens/network-map-detail-mobile.png), [320 px](ux-ui/screens/network-map-detail-narrow.png) | Устройство → фактические поля → действия → возврат |
-Оболочка и клавиатурные действия проверены (35 + 12 E2E). Камера и читаемость малой топологии доработаны в 10.53 (16 E2E + 2 unit). Следом pointer/touch и пустые состояния; итоговая приёмка карты ещё не завершена.
+Оболочка и клавиатурные действия проверены (35 + 12 E2E). Камера и читаемость малой топологии доработаны в 10.53 (16 E2E + 2 unit). Pointer/touch и пустые состояния приняты в 10.54: 39 основных + 12 source E2E, 10 unit и production frontend build. Реальные RouterOS/JWT/БД остаются внешней приёмкой; далее терминал.
diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md
index 3048fee5..78903726 100644
--- a/docs/ux-ui-progress.md
+++ b/docs/ux-ui-progress.md
@@ -26,13 +26,22 @@
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
-| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.53; оптимизатор принят локально, карта сети в работе |
+| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.54; оптимизатор и карта приняты локально, далее терминал |
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
+
+## Блок 10.54 — Pointer/touch и локальная приёмка карты (2026-10-01)
+
+- Pointer Events/capture удерживают drag за пределами canvas; основной указатель отделён от остальных, cancel/lost capture завершают жест без залипания. Touch-action отключает прокрутку только на SVG; детали продолжают прокручиваться. Фокусируемый canvas поддерживает стрелки, zoom/fit имеют клавиатурные альтернативы.
+- Закрытие деталей кнопкой/Escape возвращает фокус к выбору устройства. Пустой inventory явно предлагает серверы; поиск без результатов даёт сброс только среза. Без подтверждённого снимка экспорт недоступен; загрузка объявляется role=status.
+- Карта принята локально: 39 основных + 12 source E2E, 10 unit, TypeScript, lint и production frontend build прошли. ReUI Frame checklist повторён, адаптивные снимки обновлены. Touch проверен через реальный Chromium input protocol; физических телефонов в приёмке нет.
+- Границы: HTTP фикстуры не подтверждают RouterOS/JWT/БД; WAN пунктир остаётся моделью, Flow свежесть — время браузерного ответа; SVG флаги зависят от существующего CDN. Фильтр сохраняет приглушённые узлы как контекст, pan/zoom/drag локальные. SVG XML валиден, сторонние SVG viewers отдельно не проверены.
+- Далее 10.55: терминал — API-контракт, выбранное устройство/host из URL, достоверность выполнения и ошибки/повтор/права. Этап 10 ещё не завершён.
+
## Блок 10.53 — камера и читаемость карты (2026-10-01)
- ViewBox учитывает реальное соотношение сторон canvas; начальное вписывание включает устройства, WAN, сервисы и подписи. Оно выполняется один раз на источник и не сбрасывает ручной zoom после обновления API. Кнопка/горячая клавиша вписывают текущие объекты; wheel/drag используют ту же геометрию.
diff --git a/docs/ux-ui-stage-10.md b/docs/ux-ui-stage-10.md
index e5223436..349a49a5 100644
--- a/docs/ux-ui-stage-10.md
+++ b/docs/ux-ui-stage-10.md
@@ -301,3 +301,12 @@ Shared контракт и backend observed исходные значения/д
- Название вынесено под узел; на телефоне размер текста компенсирует SVG масштаб, длинная подпись сокращена при сохранённом полном accessible name/деталях. Страна обозначена ISO при отсутствии площадки и до загрузки внешнего изображения флага. Легенда — доступные раскрываемые обозначения вне SVG.
- WAN карточка получила Enter/Space и aria-pressed. 2 camera unit + 16 E2E layout/details/URL, TypeScript и lint прошли; после ISO fallback повторены 5 layout E2E, TypeScript и lint. Снимки обновлены.
- Далее pointer/touch, закрытие деталей с возвратом фокуса, пустой inventory/пустой срез; затем production build и локальная приёмка карты. Реальная сеть не проверена.
+
+
+## Блок 10.54 — Pointer/touch и локальная приёмка карты (2026-10-01)
+
+- Pointer Events/capture удерживают drag за пределами canvas; основной указатель отделён от остальных, cancel/lost capture завершают жест без залипания. Touch-action отключает прокрутку только на SVG; детали продолжают прокручиваться. Фокусируемый canvas поддерживает стрелки, zoom/fit имеют клавиатурные альтернативы.
+- Закрытие деталей кнопкой/Escape возвращает фокус к выбору устройства. Пустой inventory явно предлагает серверы; поиск без результатов даёт сброс только среза. Без подтверждённого снимка экспорт недоступен; загрузка объявляется role=status.
+- Карта принята локально: 39 основных + 12 source E2E, 10 unit, TypeScript, lint и production frontend build прошли. ReUI Frame checklist повторён, адаптивные снимки обновлены. Touch проверен через реальный Chromium input protocol; физических телефонов в приёмке нет.
+- Границы: HTTP фикстуры не подтверждают RouterOS/JWT/БД; WAN пунктир остаётся моделью, Flow свежесть — время браузерного ответа; SVG флаги зависят от существующего CDN. Фильтр сохраняет приглушённые узлы как контекст, pan/zoom/drag локальные. SVG XML валиден, сторонние SVG viewers отдельно не проверены.
+- Далее 10.55: терминал — API-контракт, выбранное устройство/host из URL, достоверность выполнения и ошибки/повтор/права. Этап 10 ещё не завершён.
diff --git a/docs/ux-ui/screens/network-map-detail-desktop.png b/docs/ux-ui/screens/network-map-detail-desktop.png
index 8e23ef43..da4d81e9 100644
Binary files a/docs/ux-ui/screens/network-map-detail-desktop.png and b/docs/ux-ui/screens/network-map-detail-desktop.png differ
diff --git a/docs/ux-ui/screens/network-map-detail-mobile.png b/docs/ux-ui/screens/network-map-detail-mobile.png
index 562ef923..bb76235e 100644
Binary files a/docs/ux-ui/screens/network-map-detail-mobile.png and b/docs/ux-ui/screens/network-map-detail-mobile.png differ
diff --git a/docs/ux-ui/screens/network-map-detail-narrow.png b/docs/ux-ui/screens/network-map-detail-narrow.png
index de25e802..5468bfa6 100644
Binary files a/docs/ux-ui/screens/network-map-detail-narrow.png and b/docs/ux-ui/screens/network-map-detail-narrow.png differ
diff --git a/docs/ux-ui/screens/network-map-overview-desktop.png b/docs/ux-ui/screens/network-map-overview-desktop.png
index 26711449..a9a6f38c 100644
Binary files a/docs/ux-ui/screens/network-map-overview-desktop.png and b/docs/ux-ui/screens/network-map-overview-desktop.png differ
diff --git a/docs/ux-ui/screens/network-map-overview-mobile.png b/docs/ux-ui/screens/network-map-overview-mobile.png
index 84a4f776..8417a2bb 100644
Binary files a/docs/ux-ui/screens/network-map-overview-mobile.png and b/docs/ux-ui/screens/network-map-overview-mobile.png differ
diff --git a/docs/ux-ui/screens/network-map-overview-narrow.png b/docs/ux-ui/screens/network-map-overview-narrow.png
index 5b9dce6e..81c11aab 100644
Binary files a/docs/ux-ui/screens/network-map-overview-narrow.png and b/docs/ux-ui/screens/network-map-overview-narrow.png differ
diff --git a/tests/e2e/network-map-layout.spec.ts b/tests/e2e/network-map-layout.spec.ts
index afeaed35..abb7c1b4 100644
--- a/tests/e2e/network-map-layout.spec.ts
+++ b/tests/e2e/network-map-layout.spec.ts
@@ -62,3 +62,39 @@ test("Карта: пропорции камеры, ручной масштаб
const wan=page.getByRole("button",{name:"WAN WAN",exact:true});await wan.focus();await wan.press("Enter")
await expect(page).toHaveURL(/wan=0/);await expect(wan).toHaveAttribute("aria-pressed","true")
})
+
+test("Карта: pointer capture, клавиатурный pan и возврат фокуса",async({page})=>{
+ await page.goto("/network-map");const map=page.getByRole("group",{name:"Интерактивная карта сети"}),node=page.getByRole("button",{name:/^Устройство Домашний/})
+ await expect(node).toBeVisible();await map.focus();const before=await map.getAttribute("viewBox");await map.press("ArrowDown");expect(await map.getAttribute("viewBox")).not.toBe(before)
+ await page.getByRole("combobox",{name:"Устройство на карте"}).selectOption("1")
+ const transform=await node.getAttribute("transform"),point=await node.evaluate((g:SVGGElement)=>{const matrix=g.getScreenCTM()!;return {x:matrix.e,y:matrix.f}})
+ await page.mouse.move(point.x,point.y);await page.mouse.down();await page.mouse.move(point.x+80,point.y+25,{steps:5});await page.mouse.up()
+ expect(await node.getAttribute("transform")).not.toBe(transform);await expect(page).toHaveURL(/server=1/)
+ await page.getByRole("button",{name:"Закрыть детали устройства"}).click();await expect(page.getByRole("combobox",{name:"Устройство на карте"})).toBeFocused()
+ const box=(await map.boundingBox())!,pan=await map.getAttribute("viewBox")
+ await page.mouse.move(box.x+15,box.y+box.height-50);await page.mouse.down();await page.mouse.move(box.x-30,box.y+box.height-80,{steps:5});await page.mouse.up()
+ expect(await map.getAttribute("viewBox")).not.toBe(pan)
+ const stopped=await map.getAttribute("viewBox");await page.mouse.move(box.x+100,box.y+100);expect(await map.getAttribute("viewBox")).toBe(stopped)
+})
+test("Карта: touch pan и отмена жеста без залипания",async({page})=>{
+ await page.setViewportSize({width:390,height:900});await page.goto("/network-map")
+ const map=page.getByRole("group",{name:"Интерактивная карта сети"});await expect(page.getByRole("combobox",{name:"Устройство на карте"})).toBeEnabled()
+ expect(await map.evaluate(e=>getComputedStyle(e).touchAction)).toBe("none")
+ const box=(await map.boundingBox())!,before=await map.getAttribute("viewBox"),session=await page.context().newCDPSession(page)
+ await session.send("Input.dispatchTouchEvent",{type:"touchStart",touchPoints:[{x:box.x+20,y:box.y+box.height-70,id:1}]})
+ await session.send("Input.dispatchTouchEvent",{type:"touchMove",touchPoints:[{x:box.x+70,y:box.y+box.height-100,id:1}]})
+ await session.send("Input.dispatchTouchEvent",{type:"touchCancel",touchPoints:[]})
+ expect(await map.getAttribute("viewBox")).not.toBe(before)
+ const stopped=await map.getAttribute("viewBox");await page.mouse.move(box.x+150,box.y+50);expect(await map.getAttribute("viewBox")).toBe(stopped)
+ await page.getByRole("button",{name:/^Устройство Домашний/}).click();await expect(page.getByRole("complementary",{name:"Детали объекта карты"})).toBeVisible()
+})
+test("Карта: пустой источник отличается от пустого среза",async({page})=>{
+ await page.goto("/network-map");await expect(page.getByRole("combobox",{name:"Устройство на карте"})).toBeEnabled()
+ await page.getByRole("textbox",{name:"Поиск узла"}).fill("нет такого узла");await expect(page.getByRole("status").filter({hasText:"Узлы по заданным условиям"})).toBeVisible()
+ await page.getByRole("button",{name:"Сбросить срез карты"}).click();await expect(page.getByRole("textbox",{name:"Поиск узла"})).toHaveValue("")
+ await expect(page.getByRole("button",{name:/^Устройство Домашний/})).toBeVisible()
+ await page.route("**/api/servers",r=>r.fulfill({json:[]}));await page.route("**/api/filters/gre-tunnels",r=>r.fulfill({json:{tunnels:[],failures:[]}}))
+ await page.getByRole("button",{name:"Обновить",exact:true}).click()
+ await expect(page.getByRole("status").filter({hasText:"Устройств на карте пока нет"})).toBeVisible();await expect(page.getByRole("link",{name:"Открыть серверы",exact:true})).toHaveAttribute("href","/servers")
+ await expect(page.getByRole("button",{name:"Экспорт SVG",exact:true})).toBeDisabled()
+})