style: compact icon action buttons, no horizontal scroll

Replace text action buttons with 32px icon buttons (tooltips kept
via title/aria-label): ⏻ выключить, ▶ включить, ⬇ .conf,
🔗 прямая ссылка, ⧉ копировать URL, 🗑 удалить. Actions cell
shrinks to content (width:1%, nowrap) so the table fits without
left-right scrolling.
This commit is contained in:
andrey271192
2026-06-16 19:57:49 +03:00
parent 4ebb81cede
commit 2899bba55f
2 changed files with 34 additions and 7 deletions

View File

@@ -988,31 +988,31 @@ function renderRows(clients) {
actTd.appendChild(lock); actTd.appendChild(lock);
} else { } else {
if (c.activeInConf) { if (c.activeInConf) {
actTd.appendChild(btn("Выключить", "btn small ghost", () => openDisableDialog(c))); actTd.appendChild(iconBtn("⏻", "Выключить", "btn small ghost icon", () => openDisableDialog(c)));
} else { } else {
actTd.appendChild( actTd.appendChild(
btn("Включить", "btn small primary", () => mutate("/api/clients/enable", c.clientId)) iconBtn("▶", "Включить", "btn small primary icon", () => mutate("/api/clients/enable", c.clientId))
); );
} }
if (c.exportAvailable) { if (c.exportAvailable) {
const direct = document.createElement("a"); const direct = document.createElement("a");
direct.className = "btn small ghost"; direct.className = "btn small ghost";
direct.href = clientExportGetUrl(c.clientId); direct.href = clientExportGetUrl(c.clientId);
direct.textContent = "Прямая ссылка"; direct.textContent = "🔗"; direct.classList.add("icon");
direct.rel = "noopener"; direct.rel = "noopener";
direct.title = direct.title =
"Открыть в новой вкладке — скачается .conf, если вы авторизованы в этой панели (cookies)."; "Открыть в новой вкладке — скачается .conf, если вы авторизованы в этой панели (cookies).";
actTd.appendChild(btn("Скачать .conf", "btn small ghost", () => void downloadClientConfig(c))); actTd.appendChild(iconBtn("⬇", "Скачать .conf", "btn small ghost icon", () => void downloadClientConfig(c)));
actTd.appendChild(direct); actTd.appendChild(direct);
actTd.appendChild( actTd.appendChild(
btn("Копировать URL", "btn small ghost", async () => { iconBtn("⧉", "Копировать URL", "btn small ghost icon", async () => {
const ok = await copyTextToClipboard(clientExportGetUrl(c.clientId)); const ok = await copyTextToClipboard(clientExportGetUrl(c.clientId));
setStatus(ok ? "Ссылка скопирована (вставьте в браузер, будучи залогиненным)." : "Не удалось скопировать.", !ok); setStatus(ok ? "Ссылка скопирована (вставьте в браузер, будучи залогиненным)." : "Не удалось скопировать.", !ok);
}), }),
); );
} }
actTd.appendChild(btn("Удалить", "btn small warn", () => confirmDelete(c.name, c.clientId))); actTd.appendChild(iconBtn("🗑", "Удалить", "btn small warn icon", () => confirmDelete(c.name, c.clientId)));
} }
tr.append(nameTd, ipTd, stTd, offTd, actTd); tr.append(nameTd, ipTd, stTd, offTd, actTd);
@@ -1029,6 +1029,17 @@ function btn(label, cls, onClick) {
return b; return b;
} }
function iconBtn(glyph, title, cls, onClick) {
const b = document.createElement("button");
b.type = "button";
b.className = cls;
b.textContent = glyph;
b.title = title;
b.setAttribute("aria-label", title);
b.addEventListener("click", onClick);
return b;
}
/** Для политики WARP на сервере нужны IPv4 вида 10.8.x.x/32 */ /** Для политики WARP на сервере нужны IPv4 вида 10.8.x.x/32 */
function parseIpv4Cidrs(allowedIps) { function parseIpv4Cidrs(allowedIps) {
if (!allowedIps) return []; if (!allowedIps) return [];

View File

@@ -656,7 +656,8 @@ table.clients-table {
.clients-table td.actions { .clients-table td.actions {
vertical-align: top; vertical-align: top;
min-width: 360px; width: 1%;
white-space: nowrap;
} }
table { table {
@@ -1162,3 +1163,18 @@ tr:last-child td {
white-space: pre-wrap; white-space: pre-wrap;
font-size: 0.82rem; font-size: 0.82rem;
} }
.actions { gap: 0.3rem; flex-wrap: nowrap; }
.actions .btn.icon {
width: 32px;
min-width: 32px;
height: 32px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 1rem;
line-height: 1;
}
.actions a.btn.icon { text-decoration: none; }