diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts
index f02a8a0..c01d801 100644
--- a/web/src/i18n/en.ts
+++ b/web/src/i18n/en.ts
@@ -1468,6 +1468,12 @@ const en = {
folderHint: "Creating a root auto-creates its unique root map. Add sub-regions on that canvas.",
folderHintShort: "Root folder",
outsidePeers: "{{count}} outside peer(s) linked but not on this map",
+ outsidePeersView: "Off-map NEs {{count}}",
+ outsidePeersTitle: "Outside peers",
+ outsidePeersHint: "Linked to this map but not placed. Select and add only what you need.",
+ outsidePeersVia: "via {{name}}",
+ outsidePeersFilterPh: "Filter: name / IP / via",
+ outsidePeersEmpty: "No matching outside peers",
projectNeighbors: "Project neighbors",
projectedNeighbors: "Projected {{count}} neighbor(s) onto the map",
localBrowserOnly: "this browser only",
@@ -1602,7 +1608,7 @@ const en = {
fit: "Fit view",
liveSync: "Live sync",
liveSyncOn: "Syncing…",
- liveSyncHint: "Poll the tree about every 5s and the open map every 3s (watch MCP create/draw). Off by default. No need to open a map first",
+ liveSyncHint: "Poll the tree about every 5s and the open map every 3s (watch MCP create/draw). Off by default",
fullscreen: "Fullscreen",
exitFullscreen: "Exit fullscreen",
display: "Display",
@@ -1634,6 +1640,7 @@ const en = {
edgeFlow: "Edge flow",
collapseSidebar: "Collapse sidebar",
expandSidebar: "Expand sidebar",
+ resizeSidebar: "Drag to resize sidebar",
hideAdded: "Hide added",
paletteEmpty: "No matching NEs.",
paletteHiddenAll: "Added NEs are hidden; uncheck to show them again.",
diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts
index fd465e2..d4782ad 100644
--- a/web/src/i18n/zh.ts
+++ b/web/src/i18n/zh.ts
@@ -1457,6 +1457,12 @@ const zh = {
folderHint: "新建根时自动创建唯一「根图」。子区域请在根图画布上创建。",
folderHintShort: "根目录",
outsidePeers: "图外对端 {{count}} 个(有链路未上图)",
+ outsidePeersView: "未上图网元 {{count}}",
+ outsidePeersTitle: "图外对端",
+ outsidePeersHint: "与画布有链路但未上图。勾选后按需添加,不必一次全部投影。",
+ outsidePeersVia: "经 {{name}}",
+ outsidePeersFilterPh: "筛选:名称 / IP / 经谁",
+ outsidePeersEmpty: "没有匹配的图外对端",
projectNeighbors: "投影邻居上图",
projectedNeighbors: "已投影 {{count}} 个邻居上图",
localBrowserOnly: "仅本浏览器",
@@ -1587,7 +1593,7 @@ const zh = {
fit: "适应画布",
liveSync: "实时同步",
liveSyncOn: "同步中…",
- liveSyncHint: "开启后约每 5 秒刷新左侧树、每 3 秒刷新已打开的图(观看 MCP 建图/画拓扑);默认关闭。不需要先打开画布",
+ liveSyncHint: "开启后约每 5 秒刷新左侧树、每 3 秒刷新当前画布(观看 MCP 建图/画拓扑);默认关闭",
fullscreen: "全屏显示",
exitFullscreen: "退出全屏",
display: "显示",
@@ -1619,6 +1625,7 @@ const zh = {
edgeFlow: "链路流动",
collapseSidebar: "折叠侧栏",
expandSidebar: "展开侧栏",
+ resizeSidebar: "拖动调整侧栏宽度",
hideAdded: "隐藏已添加",
paletteEmpty: "没有匹配的网元。",
paletteHiddenAll: "已添加网元已隐藏;取消勾选可重新显示。",
diff --git a/web/src/index.css b/web/src/index.css
index 445fad8..a6b4481 100644
--- a/web/src/index.css
+++ b/web/src/index.css
@@ -2,6 +2,35 @@
box-sizing: border-box;
}
+html {
+ color-scheme: dark;
+ scrollbar-color: #475569 #0f172a;
+ scrollbar-width: thin;
+}
+
+*::-webkit-scrollbar {
+ width: 10px;
+ height: 10px;
+}
+
+*::-webkit-scrollbar-track {
+ background: #0f172a;
+}
+
+*::-webkit-scrollbar-thumb {
+ background: #475569;
+ border: 2px solid #0f172a;
+ border-radius: 999px;
+}
+
+*::-webkit-scrollbar-thumb:hover {
+ background: #64748b;
+}
+
+*::-webkit-scrollbar-corner {
+ background: #0f172a;
+}
+
body {
margin: 0;
font-family: "Segoe UI", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
@@ -4305,8 +4334,9 @@ pre {
/* ---- Topology — dark sidebar + canvas (shared chrome tokens) ---- */
.topo-page {
+ --topo-sidebar-w: 260px;
display: grid;
- grid-template-columns: 260px 1fr;
+ grid-template-columns: var(--topo-sidebar-w) 1fr;
gap: 12px;
height: calc(100vh - 48px);
min-height: 520px;
@@ -4320,7 +4350,13 @@ pre {
grid-template-columns: 36px 1fr;
}
+.topo-page.is-sidebar-resizing {
+ transition: none;
+ cursor: col-resize;
+}
+
.topo-sidebar {
+ position: relative;
display: flex;
flex-direction: column;
gap: 10px;
@@ -4334,6 +4370,35 @@ pre {
box-shadow: 0 1px 2px rgba(15, 27, 45, 0.06);
}
+.topo-sidebar__resizer {
+ position: absolute;
+ top: 0;
+ right: -8px;
+ z-index: 6;
+ width: 12px;
+ height: 100%;
+ cursor: col-resize;
+ touch-action: none;
+}
+
+.topo-sidebar__resizer::after {
+ content: "";
+ position: absolute;
+ top: 12%;
+ bottom: 12%;
+ left: 50%;
+ width: 2px;
+ transform: translateX(-50%);
+ border-radius: 1px;
+ background: transparent;
+ transition: background 0.12s ease;
+}
+
+.topo-sidebar__resizer:hover::after,
+.topo-sidebar__resizer.is-dragging::after {
+ background: rgba(59, 130, 246, 0.75);
+}
+
.topo-page.is-sidebar-collapsed .topo-sidebar {
padding: 4px;
align-items: stretch;
@@ -4366,29 +4431,6 @@ pre {
flex: 1;
}
-.topo-sidebar__rail--live {
- flex: 0 0 auto;
- padding: 10px 0;
-}
-
-.topo-sidebar__rail--live.is-on {
- color: #93c5fd;
- background: rgba(37, 99, 235, 0.2);
-}
-
-.topo-sidebar__rail-live-dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: currentColor;
- opacity: 0.45;
-}
-
-.topo-sidebar__rail--live.is-on .topo-sidebar__rail-live-dot {
- opacity: 1;
- box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);
-}
-
.topo-sidebar__rail .topo-sidebar__rail-icon {
width: 28px;
height: 28px;
@@ -4479,17 +4521,6 @@ pre {
min-width: 0;
}
-.topo-sidebar__live {
- display: flex;
- align-items: center;
- margin-top: 2px;
-}
-
-.topo-sidebar__live .btn {
- width: 100%;
- justify-content: center;
-}
-
.topo-tree-search__bar > .input {
flex: 1 1 auto;
min-width: 0;
@@ -6028,36 +6059,29 @@ pre {
justify-content: flex-start;
flex-wrap: wrap;
gap: 8px;
- padding-top: 6px;
- border-top: 1px solid rgba(148, 163, 184, 0.16);
+ padding-top: 4px;
+ border-top: 1px solid rgba(148, 163, 184, 0.12);
}
.topo-toolbar__group + .topo-toolbar__group,
-.topo-toolbar__panel + .topo-toolbar__group,
+.topo-toolbar__cluster + .topo-toolbar__group,
.topo-toolbar__group + .topo-toolbar__group--find,
-.topo-toolbar__panel + .topo-toolbar__group--find {
+.topo-toolbar__cluster + .topo-toolbar__group--find {
margin-left: 2px;
padding-left: 10px;
border-left: 1px solid rgba(148, 163, 184, 0.2);
}
-.topo-toolbar__panel {
+/* Flat cluster: no nested chrome — only a separator from tool modes. */
+.topo-toolbar__cluster {
display: inline-flex;
align-items: center;
- gap: 6px;
+ gap: 8px;
flex-shrink: 0;
- padding: 3px 6px;
- background: rgba(2, 8, 23, 0.55);
- border: 1px solid rgba(148, 163, 184, 0.22);
- border-radius: 6px;
-}
-
-.topo-toolbar__panel .topo-tools {
- background: rgba(15, 23, 42, 0.8);
-}
-
-.topo-toolbar__panel .topo-toolbar__display > summary {
- background: #0b1220;
+ min-width: 0;
+ margin-left: 2px;
+ padding-left: 10px;
+ border-left: 1px solid rgba(148, 163, 184, 0.2);
}
.topo-toolbar__group--find {
@@ -6196,11 +6220,8 @@ pre {
.topo-tools {
display: inline-flex;
+ align-items: center;
gap: 2px;
- padding: 2px;
- background: rgba(2, 8, 23, 0.55);
- border: 1px solid rgba(148, 163, 184, 0.22);
- border-radius: 6px;
flex-shrink: 0;
}
@@ -6213,8 +6234,8 @@ pre {
color: #94a3b8;
font-size: 12px;
font-weight: 600;
- padding: 4px 8px;
- border-radius: 4px;
+ padding: 5px 9px;
+ border-radius: 6px;
cursor: pointer;
line-height: 1.2;
}
@@ -6881,6 +6902,24 @@ pre {
background: var(--topo-canvas-bg, #0f172a);
--xy-edge-label-background-color: transparent;
--xy-edge-label-color: var(--topo-edge-label-color, #e2e8f0);
+ /* Defaults = dark canvas; TopologyPage overrides via canvasFloatChromeVars. */
+ --topo-float-bg: rgba(15, 23, 42, 0.92);
+ --topo-float-border: rgba(148, 163, 184, 0.22);
+ --topo-float-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
+ --topo-float-fg: #e2e8f0;
+ --topo-float-muted: #94a3b8;
+ --topo-float-btn-bg: #0b1220;
+ --topo-float-btn-border: rgba(148, 163, 184, 0.28);
+ --topo-float-btn-hover: #152033;
+ --topo-float-active-bg: rgba(37, 99, 235, 0.28);
+ --topo-float-active-fg: #93c5fd;
+ --topo-float-active-border: #3b82f6;
+ --topo-float-control-bg: #0f172a;
+ --topo-float-control-hover: #1e293b;
+ --topo-float-control-fill: #cbd5e1;
+ --topo-float-input-bg: #0b1220;
+ --topo-float-input-fg: #e2e8f0;
+ --topo-float-input-border: rgba(148, 163, 184, 0.28);
}
.topo-canvas:fullscreen,
@@ -6894,31 +6933,39 @@ pre {
.topo-canvas .react-flow__controls {
display: flex;
flex-direction: column;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
- border: 1px solid rgba(148, 163, 184, 0.28);
+ box-shadow: var(--topo-float-shadow);
+ border: 0;
border-radius: 6px;
overflow: hidden;
+ background: var(--topo-float-bg);
}
.topo-canvas .react-flow__controls-button {
- background: #0f172a;
- border-bottom-color: rgba(148, 163, 184, 0.22);
- fill: #cbd5e1;
+ background: transparent;
+ border: 0;
+ border-bottom: 1px solid var(--topo-float-border);
+ color: var(--topo-float-control-fill);
+ fill: var(--topo-float-control-fill);
+}
+
+.topo-canvas .react-flow__controls-button:last-child {
+ border-bottom: 0;
}
.topo-canvas .react-flow__controls-button:hover {
- background: #1e293b;
+ background: var(--topo-float-control-hover);
}
-.topo-canvas .react-flow__controls-button svg {
- fill: #cbd5e1;
+.topo-canvas .react-flow__controls-button svg,
+.topo-canvas .react-flow__controls-button svg path {
+ fill: currentColor;
}
.topo-canvas .react-flow__minimap {
- background: #0b1220;
- border: 1px solid rgba(148, 163, 184, 0.28);
- border-radius: 6px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
+ border: 0;
+ border-radius: 8px;
+ overflow: hidden;
+ box-shadow: var(--topo-float-shadow);
}
.topo-canvas .react-flow__controls-button.topo-fs-control {
@@ -6928,16 +6975,13 @@ pre {
.topo-canvas .react-flow__controls-button.topo-fs-control svg {
max-width: 14px;
max-height: 14px;
+ width: 14px;
+ height: 14px;
+ display: block;
+ color: inherit;
}
-.topo-toolbar__panel--view {
- margin-left: 2px;
- padding-left: 10px;
- border-left: 1px solid rgba(148, 163, 184, 0.2);
- gap: 8px;
-}
-
-.topo-toolbar__panel--view .topo-toolbar__group--find {
+.topo-toolbar__cluster .topo-toolbar__group--find {
margin-left: 0;
padding-left: 0;
border-left: 0;
@@ -6953,9 +6997,10 @@ pre {
gap: 6px;
padding: 8px;
border-radius: 10px;
- background: rgba(15, 23, 42, 0.92);
- border: 1px solid rgba(148, 163, 184, 0.22);
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
+ background: var(--topo-float-bg);
+ border: 1px solid var(--topo-float-border);
+ box-shadow: var(--topo-float-shadow);
+ color: var(--topo-float-fg);
pointer-events: auto;
}
@@ -6970,9 +7015,10 @@ pre {
gap: 6px;
padding: 6px 8px;
border-radius: 10px;
- background: rgba(15, 23, 42, 0.92);
- border: 1px solid rgba(148, 163, 184, 0.22);
- box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35);
+ background: var(--topo-float-bg);
+ border: 1px solid var(--topo-float-border);
+ box-shadow: var(--topo-float-shadow);
+ color: var(--topo-float-fg);
pointer-events: auto;
max-width: min(520px, calc(100% - 24px));
}
@@ -7006,32 +7052,71 @@ pre {
right: 12px;
}
+/* Beat `.app-main button { background:#0b1220 }` so inactive chips stay ghost. */
+.app-main .topo-fs-toolbar__btn,
+.topo-canvas .topo-fs-toolbar__btn,
.topo-fs-toolbar__btn {
display: inline-flex;
align-items: center;
gap: 6px;
- padding: 6px 10px;
- border-radius: 8px;
- border: 1px solid rgba(148, 163, 184, 0.28);
- background: #0b1220;
- color: #e2e8f0;
+ height: auto;
+ padding: 6px 10px !important;
+ border-radius: 8px !important;
+ border: 1px solid transparent !important;
+ background: transparent !important;
+ color: var(--topo-float-muted) !important;
+ box-shadow: none !important;
font-size: 12px;
+ font-weight: 600;
+ line-height: 1.2;
cursor: pointer;
}
+.app-main .topo-fs-toolbar__btn:hover,
+.topo-canvas .topo-fs-toolbar__btn:hover,
.topo-fs-toolbar__btn:hover {
- background: #152033;
+ background: var(--topo-float-btn-hover) !important;
+ color: var(--topo-float-fg) !important;
}
+.app-main .topo-fs-toolbar__btn.is-active,
+.topo-canvas .topo-fs-toolbar__btn.is-active,
.topo-fs-toolbar__btn.is-active {
- border-color: #3b82f6;
- background: rgba(37, 99, 235, 0.28);
- color: #93c5fd;
+ border-color: var(--topo-float-active-border) !important;
+ background: var(--topo-float-active-bg) !important;
+ color: var(--topo-float-active-fg) !important;
}
+.app-main .topo-fs-toolbar__btn kbd,
+.topo-canvas .topo-fs-toolbar__btn kbd,
.topo-fs-toolbar__btn kbd {
font-size: 10px;
opacity: 0.7;
+ border-color: currentColor;
+ background: transparent;
+ color: inherit;
+ padding: 0 3px;
+}
+
+/* Beat global dark toolbar overrides when canvas bg is light. */
+.topo-canvas.is-canvas-light .topo-view-tools .topo-toolbar__find,
+.topo-canvas.is-canvas-light .topo-view-tools .topo-toolbar__select,
+.topo-canvas.is-canvas-light .topo-view-tools .topo-toolbar__display > summary {
+ background: var(--topo-float-input-bg) !important;
+ border-color: var(--topo-float-input-border) !important;
+ color: var(--topo-float-input-fg) !important;
+}
+
+.topo-canvas.is-canvas-light .topo-view-tools .topo-toolbar__find::placeholder {
+ color: var(--topo-float-muted);
+}
+
+.topo-canvas .topo-view-tools .topo-toolbar__find,
+.topo-canvas .topo-view-tools .topo-toolbar__select,
+.topo-canvas .topo-view-tools .topo-toolbar__display > summary {
+ background: var(--topo-float-input-bg);
+ border-color: var(--topo-float-input-border);
+ color: var(--topo-float-input-fg);
}
.topo-canvas__empty {
@@ -7330,15 +7415,36 @@ pre {
pointer-events: none;
}
-.topo-outside-peers {
- display: flex;
- flex-direction: column;
- gap: 6px;
- padding: 0 10px 10px;
+.app-main .topo-toolbar__outside-peers,
+.topo-toolbar__outside-peers {
+ margin-left: auto;
+ flex-shrink: 0;
+ white-space: nowrap;
+ width: auto;
+ height: 30px;
+ padding: 0 10px;
+ font-size: 12px;
+ font-weight: 600;
+ line-height: 1;
+ border-radius: 6px;
+ border: 1px solid rgba(148, 163, 184, 0.28);
+ background: #0b1220;
+ color: #94a3b8;
}
-.topo-outside-peers .panel__hint {
- margin: 0;
+.app-main .topo-toolbar__outside-peers:hover,
+.topo-toolbar__outside-peers:hover {
+ background: #152033;
+ color: #e2e8f0;
+}
+
+.topo-modal__panel--wide {
+ width: min(560px, calc(100vw - 32px));
+}
+
+.topo-modal__count {
+ font-weight: 500;
+ color: #94a3b8;
}
.topo-node--cisco {
@@ -7659,6 +7765,10 @@ pre {
height: auto;
}
+ .topo-sidebar__resizer {
+ display: none;
+ }
+
.topo-sidebar {
max-height: 40vh;
}
diff --git a/web/src/pages/TopologyPage.tsx b/web/src/pages/TopologyPage.tsx
index 55df30c..f266680 100644
--- a/web/src/pages/TopologyPage.tsx
+++ b/web/src/pages/TopologyPage.tsx
@@ -8,6 +8,7 @@ import {
useRef,
useState,
type CSSProperties,
+ type MouseEvent as ReactMouseEvent,
type ReactNode,
} from "react";
import { createPortal } from "react-dom";
@@ -89,6 +90,39 @@ function isPlaceholderSource(source: string | undefined, neIp: string): boolean
const LAST_LEAF_KEY = "netx.topology.lastLeafViewId";
const TREE_EXPAND_KEY = "netx.topology.treeExpanded";
+const SIDEBAR_WIDTH_KEY = "netx.topology.sidebarWidth";
+const SIDEBAR_WIDTH_DEFAULT = 260;
+const SIDEBAR_WIDTH_MIN = 180;
+const SIDEBAR_WIDTH_MAX = 560;
+
+function loadSidebarWidth(): number {
+ try {
+ const n = Number(localStorage.getItem(SIDEBAR_WIDTH_KEY));
+ if (Number.isFinite(n) && n >= SIDEBAR_WIDTH_MIN) {
+ return Math.min(Math.round(n), SIDEBAR_WIDTH_MAX);
+ }
+ } catch {
+ /* ignore */
+ }
+ return SIDEBAR_WIDTH_DEFAULT;
+}
+
+function saveSidebarWidth(width: number): void {
+ try {
+ localStorage.setItem(SIDEBAR_WIDTH_KEY, String(Math.round(width)));
+ } catch {
+ /* ignore */
+ }
+}
+
+function clampSidebarWidth(width: number, pageWidth?: number): number {
+ const maxByPage =
+ pageWidth && pageWidth > 0
+ ? Math.max(SIDEBAR_WIDTH_MIN, Math.floor(pageWidth * 0.55))
+ : SIDEBAR_WIDTH_MAX;
+ const maxW = Math.min(SIDEBAR_WIDTH_MAX, maxByPage);
+ return Math.max(SIDEBAR_WIDTH_MIN, Math.min(maxW, Math.round(width)));
+}
function findViewInRegion(
regions: TopologyTreeFolderItem[],
@@ -677,6 +711,61 @@ function isHexColor(value: string): boolean {
return /^#[0-9a-fA-F]{6}$/.test(value);
}
+function canvasBgLuminance(bg: string): number {
+ const hex = String(bg || "").replace("#", "").trim();
+ if (hex.length !== 6) return 0.1;
+ const r = parseInt(hex.slice(0, 2), 16);
+ const g = parseInt(hex.slice(2, 4), 16);
+ const b = parseInt(hex.slice(4, 6), 16);
+ if (![r, g, b].every((n) => Number.isFinite(n))) return 0.1;
+ return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
+}
+
+/** Floating chrome (fs toolbar / view tools / controls) follows canvas bg like MiniMap. */
+function canvasFloatChromeVars(bg: string): CSSProperties {
+ const light = canvasBgLuminance(bg) > 0.55;
+ if (light) {
+ return {
+ "--topo-float-bg": "rgba(255, 255, 255, 0.92)",
+ "--topo-float-border": "rgba(15, 23, 42, 0.08)",
+ "--topo-float-shadow": "0 8px 24px rgba(15, 23, 42, 0.12)",
+ "--topo-float-fg": "#0f172a",
+ "--topo-float-muted": "#64748b",
+ "--topo-float-btn-bg": "transparent",
+ "--topo-float-btn-border": "transparent",
+ "--topo-float-btn-hover": "rgba(15, 23, 42, 0.06)",
+ "--topo-float-active-bg": "rgba(37, 99, 235, 0.16)",
+ "--topo-float-active-fg": "#1d4ed8",
+ "--topo-float-active-border": "#3b82f6",
+ "--topo-float-control-bg": "transparent",
+ "--topo-float-control-hover": "rgba(15, 23, 42, 0.06)",
+ "--topo-float-control-fill": "#334155",
+ "--topo-float-input-bg": "#ffffff",
+ "--topo-float-input-fg": "#0f172a",
+ "--topo-float-input-border": "rgba(15, 23, 42, 0.16)",
+ } as CSSProperties;
+ }
+ return {
+ "--topo-float-bg": "rgba(15, 23, 42, 0.92)",
+ "--topo-float-border": "rgba(148, 163, 184, 0.18)",
+ "--topo-float-shadow": "0 8px 24px rgba(0, 0, 0, 0.35)",
+ "--topo-float-fg": "#e2e8f0",
+ "--topo-float-muted": "#94a3b8",
+ "--topo-float-btn-bg": "transparent",
+ "--topo-float-btn-border": "transparent",
+ "--topo-float-btn-hover": "rgba(30, 41, 59, 0.9)",
+ "--topo-float-active-bg": "rgba(37, 99, 235, 0.28)",
+ "--topo-float-active-fg": "#93c5fd",
+ "--topo-float-active-border": "#3b82f6",
+ "--topo-float-control-bg": "transparent",
+ "--topo-float-control-hover": "rgba(30, 41, 59, 0.9)",
+ "--topo-float-control-fill": "#cbd5e1",
+ "--topo-float-input-bg": "#0b1220",
+ "--topo-float-input-fg": "#e2e8f0",
+ "--topo-float-input-border": "rgba(148, 163, 184, 0.28)",
+ } as CSSProperties;
+}
+
function loadCanvasBg(): string {
try {
const raw = String(localStorage.getItem(CANVAS_BG_KEY) || "").trim().toLowerCase();
@@ -1054,7 +1143,15 @@ export function TopologyPage() {
loadBoolFlag(DISCOVER_PROJECT_NEIGHBORS_KEY, true),
);
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
+ const [sidebarWidth, setSidebarWidth] = useState(() => loadSidebarWidth());
+ const [sidebarResizing, setSidebarResizing] = useState(false);
+ const sidebarDragRef = useRef<{ startX: number; startW: number } | null>(null);
+ const topoPageRef = useRef
- {t("topology.outsidePeers").replace("{{count}}", String(outsidePeers.length))}
-
{t("topology.outsidePeersHint")}
+ { + setOutsidePeerQuery(e.target.value); + }} + placeholder={t("topology.outsidePeersFilterPh")} + disabled={outsidePeersAdding} + autoFocus + /> + {outsidePeersVisible.length > 0 ? ( +