fix(map): восстановить легенду и убрать лишние рамки фокуса
This commit is contained in:
1 parent
1c11a133f3
commit
eb24a08981
5 files changed
+155
-10
No files matched your search
@@ -11,6 +11,7 @@ import {Dialog,DialogContent,DialogHeader,DialogTitle,DialogDescription} from "@
|
||||
import {usePrefersReducedMotion} from "@/hooks/use-prefers-reduced-motion"
|
||||
import {toast} from "sonner"
|
||||
import { PageHeader } from "@/components/page-header"
|
||||
import { NetworkMapLegend } from "@/components/network-map-legend"
|
||||
import {
|
||||
servers as mockServers,
|
||||
greTunnels as mockGreTunnels,
|
||||
@@ -556,7 +557,7 @@ function GreEdgeMetricBadge({
|
||||
"Клик — панель справа."
|
||||
return (
|
||||
<g
|
||||
role="button" aria-label={"Метрики "+(outerSummary??"связи")} tabIndex={0} className="focus-visible:outline-2 focus-visible:outline-ring"
|
||||
role="button" aria-label={"Метрики "+(outerSummary??"связи")} tabIndex={0} className="network-map-focusable"
|
||||
onKeyDown={e=>{if(e.key==="Enter"||e.key===" "){e.preventDefault();e.stopPropagation();onOpen(e)}}}
|
||||
transform={`translate(${mx},${my})`}
|
||||
style={{ cursor: "pointer" }}
|
||||
@@ -564,6 +565,7 @@ function GreEdgeMetricBadge({
|
||||
onClick={(e) => { e.stopPropagation(); onOpen(e) }}
|
||||
>
|
||||
<title>{hint}</title>
|
||||
<rect className="network-map-focus-ring" x={-bw / 2 - 4} y={-bh / 2 - 4} width={bw + 8} height={bh + 8} rx="9" fill="none" stroke="var(--ring)" strokeWidth="2" />
|
||||
<rect
|
||||
x={-bw / 2}
|
||||
y={-bh / 2}
|
||||
@@ -607,7 +609,7 @@ function NetflowRateBadge({
|
||||
const bh = showDir ? 32 : 20
|
||||
return (
|
||||
<g
|
||||
role={onOpen?"button":undefined} aria-label={onOpen?"Поток NetFlow между узлами":undefined} tabIndex={onOpen?0:undefined} className="focus-visible:outline-2 focus-visible:outline-ring"
|
||||
role={onOpen?"button":undefined} aria-label={onOpen?"Поток NetFlow между узлами":undefined} tabIndex={onOpen?0:undefined} className="network-map-focusable"
|
||||
onKeyDown={e=>{if(onOpen&&(e.key==="Enter"||e.key===" ")){e.preventDefault();e.stopPropagation();onOpen(e)}}}
|
||||
transform={`translate(${mx},${my})`}
|
||||
style={{ cursor: onOpen ? "pointer" : "default" }}
|
||||
@@ -617,6 +619,7 @@ function NetflowRateBadge({
|
||||
<title>
|
||||
Поток NetFlow между узлами (как в «Трафик»: 5 мин, без overlay/mesh). Скорость канала — отдельно.
|
||||
</title>
|
||||
<rect className="network-map-focus-ring" x={-bw / 2 - 4} y={-bh / 2 - 4} width={bw + 8} height={bh + 8} rx="9" fill="none" stroke="var(--ring)" strokeWidth="2" />
|
||||
<rect
|
||||
x={-bw / 2}
|
||||
y={-bh / 2}
|
||||
@@ -687,7 +690,7 @@ function ServerNode({ labelScale = 1, n, isSel, isVis, isDragged, hideCatalogLat
|
||||
const ts = TYPE_STYLE[n.type]
|
||||
|
||||
return (
|
||||
<g className="focus-visible:outline-2 focus-visible:outline-ring" role="button" aria-label={"Устройство "+n.name} aria-pressed={isSel} tabIndex={isVis?0:-1}
|
||||
<g className="network-map-focusable" role="button" aria-label={"Устройство "+n.name} aria-pressed={isSel} tabIndex={isVis?0:-1}
|
||||
onKeyDown={e=>{if(e.key==="Enter"||e.key===" "){e.preventDefault();e.stopPropagation();onClick()}}}
|
||||
transform={`translate(${n.x},${n.y})`}
|
||||
style={{ cursor: isDragged ? "grabbing" : "grab", transition: isDragged ? "none" : "opacity 0.25s" }}
|
||||
@@ -699,6 +702,7 @@ function ServerNode({ labelScale = 1, n, isSel, isVis, isDragged, hideCatalogLat
|
||||
onMouseEnter={() => onHoverChange(true)}
|
||||
onMouseLeave={() => onHoverChange(false)}
|
||||
>
|
||||
<circle className="network-map-focus-ring" r={ts.r + 6} fill="none" stroke="var(--ring)" strokeWidth="2" />
|
||||
{n.status === "online" && isVis && (
|
||||
<circle r={ts.r + 14} fill={ss.glow} opacity="0.7">
|
||||
</circle>
|
||||
@@ -707,7 +711,7 @@ function ServerNode({ labelScale = 1, n, isSel, isVis, isDragged, hideCatalogLat
|
||||
<circle r={ts.r + 12} fill={ss.glow} />
|
||||
)}
|
||||
{isSel && (
|
||||
<circle r={ts.r + 10} fill="none" stroke="var(--foreground)" strokeWidth="1.5" strokeDasharray="4 3" />
|
||||
<circle r={ts.r + 10} fill="none" stroke="var(--foreground)" strokeWidth="1.5" strokeDasharray="4 3" opacity="0.45" />
|
||||
)}
|
||||
<circle r={ts.r} fill={ss.fill} stroke={ss.stroke} strokeWidth={isSel ? 2.5 : 1.8} />
|
||||
<g transform={`translate(${ts.r - 10},-${ts.r - 10})`}>
|
||||
@@ -807,7 +811,7 @@ function ServiceNode({
|
||||
const bh = MAP_SERVICE_NODE_H
|
||||
const flagIso = destMode === "countries" && iso && iso !== "Прочее" ? iso : ""
|
||||
return (
|
||||
<g role="button" aria-label={(destMode==="countries"?"Страна ":"Сервис ")+label} aria-pressed={isSel} tabIndex={isVis?0:-1} className="focus-visible:outline-2 focus-visible:outline-ring"
|
||||
<g role="button" aria-label={(destMode==="countries"?"Страна ":"Сервис ")+label} aria-pressed={isSel} tabIndex={isVis?0:-1} className="network-map-focusable"
|
||||
onKeyDown={e=>{if(e.key==="Enter"||e.key===" "){e.preventDefault();e.stopPropagation();onClick()}}}
|
||||
transform={`translate(${x},${y})`}
|
||||
style={{ cursor: isDragged ? "grabbing" : "grab", transition: isDragged ? "none" : "opacity 0.25s" }}
|
||||
@@ -816,6 +820,7 @@ function ServiceNode({
|
||||
onClick={(e) => { e.stopPropagation(); onClick() }}
|
||||
>
|
||||
<title>{`${label} · ${serviceSharePct(share)} ${shareLabel ?? "payload окна"}`}</title>
|
||||
<rect className="network-map-focus-ring" x={-bw / 2 - 3} y={-bh / 2 - 3} width={bw + 6} height={bh + 6} rx="9" fill="none" stroke="var(--ring)" strokeWidth="2" />
|
||||
{isSel && (
|
||||
<rect
|
||||
x={-bw / 2 - 6}
|
||||
@@ -925,12 +930,13 @@ function WanSatNode({ x, y, wan, color, active, isSel, isDragged, onSelect, onPo
|
||||
}) {
|
||||
const r = 18
|
||||
return (
|
||||
<g role="button" aria-label={"Аплинк "+wan.name} aria-pressed={isSel} tabIndex={0} className="focus-visible:outline-2 focus-visible:outline-ring"
|
||||
<g role="button" aria-label={"Аплинк "+wan.name} aria-pressed={isSel} tabIndex={0} className="network-map-focusable"
|
||||
onKeyDown={e=>{if(e.key==="Enter"||e.key===" "){e.preventDefault();e.stopPropagation();onSelect()}}}
|
||||
transform={`translate(${x},${y})`}
|
||||
style={{ cursor: isDragged ? "grabbing" : "grab" }}
|
||||
onPointerDown={e => { e.stopPropagation(); onPointerDown(e) }}
|
||||
onClick={e => { e.stopPropagation(); onSelect() }}>
|
||||
<circle className="network-map-focus-ring" r={r + 4} fill="none" stroke="var(--ring)" strokeWidth="2" />
|
||||
{active && (
|
||||
<circle r={r + 6} fill={color} opacity="0.12">
|
||||
</circle>
|
||||
@@ -1718,6 +1724,7 @@ function NetworkMapContent() {
|
||||
if (!svg) return
|
||||
// Clone with full viewBox (reset pan/zoom)
|
||||
const clone = svg.cloneNode(true) as SVGSVGElement
|
||||
clone.querySelectorAll(".network-map-focus-ring").forEach(ring => ring.remove())
|
||||
clone.setAttribute("viewBox", `0 0 ${W} ${H}`)
|
||||
clone.setAttribute("width", String(W))
|
||||
clone.setAttribute("height", String(H))
|
||||
@@ -2252,10 +2259,7 @@ function NetworkMapContent() {
|
||||
|
||||
{/* ── Canvas ── */}
|
||||
<div ref={canvasRef} className={cn("flex-1 min-h-0 min-w-0 relative overflow-hidden bg-background select-none", (selected || selectedGreEdge || selectedService) && "hidden lg:block")}>
|
||||
<details className="absolute top-3 left-3 z-20 rounded-md border bg-popover p-2 text-xs shadow-sm">
|
||||
<summary className="cursor-pointer focus-visible:outline-2 focus-visible:outline-ring">Обозначения карты</summary>
|
||||
<div className="mt-2 space-y-1"><p>HR · домашний роутер</p><p>JH · промежуточный узел</p><p>EN · выходной узел</p><p>Круг · устройство / WAN</p><p>Прямоугольник · сервис / страна</p><p>Пунктир WAN → JH · модель связности</p><p>Цвет · статус; неизвестно ≠ оффлайн</p></div>
|
||||
</details>
|
||||
<NetworkMapLegend />
|
||||
<p id="network-map-help" className="sr-only">Стрелки перемещают карту, плюс и минус меняют масштаб, F вписывает объекты. Tab выбирает объект, Enter открывает детали.</p>
|
||||
<svg
|
||||
ref={svgRef} role="group" aria-label="Интерактивная карта сети" tabIndex={0} aria-describedby="network-map-help"
|
||||
|
||||
@@ -2,6 +2,18 @@
|
||||
@import "tw-animate-css";
|
||||
@import "shadcn/tailwind.css";
|
||||
|
||||
/* SVG focus follows the object shape, never the text/foreignObject bounding box. */
|
||||
.network-map-focusable {
|
||||
outline: none;
|
||||
}
|
||||
.network-map-focus-ring {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.network-map-focusable:focus-visible > .network-map-focus-ring {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
|
||||
Reference in new issue
Block a user