diff --git a/frontend/docs/TABLE_UX.md b/frontend/docs/TABLE_UX.md
new file mode 100644
index 0000000..ca5aec0
--- /dev/null
+++ b/frontend/docs/TABLE_UX.md
@@ -0,0 +1,43 @@
+# Таблицы: UX/UI по Tabler и проекту
+
+Рекомендации по формированию таблиц в приложении — [Tabler Tables](https://docs.tabler.io/ui/components/tables/), [Preview](https://preview.tabler.io/tables.html).
+
+## Классы (Tabler)
+
+- **Базовые:** `table card-table table-vcenter table-nowrap mb-0`
+- **Чередование строк:** `table-striped` — улучшает читаемость
+- **Компактный режим:** `table-sm` — меньше отступы в ячейках
+- **Обёртка:** `table-responsive` вокруг `
`
+- **Обрезка длинного текста:** для ячейки использовать `td-truncate` (Tabler) или `text-truncate` + `max-width`
+
+## Компонент DataTable
+
+Используйте `DataTable` там, где подходит декларативное описание колонок и общие действия (сортировка, выбор, пагинация):
+
+- `columns`: `{ key, title, sortable?, icon?, render?, tooltip? }` — для подсказки по заголовку задайте `tooltip`
+- `striped={true}` по умолчанию
+- `compact={true}` — компактные ячейки
+- Колонка «Действия» получает класс `td-actions` (фиксированная минимальная ширина в проекте)
+
+## Информативность и лаконичность
+
+1. **Заголовки колонок** — короткие, при необходимости уточнение через `tooltip` (например, значение иконки замка).
+2. **Действия в строке** — иконки с `Tooltip` и `aria-label`; колонка «Действия» справа, без лишнего текста.
+3. **Копирование** — кнопка/иконка копирования рядом с ячейкой (IP, домен и т.д.) вместо дублирования в «Действия», если это основной сценарий.
+4. **Пустое состояние** — через `emptyState` в DataTable или свой блок с призывом к действию (импорт, добавление).
+
+## Доступность
+
+- У заголовков: `scope="col"`.
+- У таблицы: `role="grid"` при семантической сетке данных.
+- Кнопки действий: `aria-label` и/или Tooltip.
+
+## Где таблица своя, а не DataTable
+
+В IPRangesManager, DomainsNewManager, ServerManager и др. таблицы рендерятся вручную. Для единообразия:
+
+- Те же классы: `table card-table table-vcenter table-nowrap mb-0`, при желании `table-striped`.
+- Заголовки: `th` с `scope="col"`, короткий текст, при необходимости иконка и тултип.
+- Колонка действий: класс `td-actions` у `th` и `td`, кнопки с тултипами.
+
+При рефакторинге страниц можно постепенно переводить такие таблицы на DataTable с описанием `columns` и общих обработчиков.
diff --git a/frontend/src/components/DataTable.jsx b/frontend/src/components/DataTable.jsx
index 8da21f6..a4ad188 100644
--- a/frontend/src/components/DataTable.jsx
+++ b/frontend/src/components/DataTable.jsx
@@ -34,6 +34,8 @@ import Tooltip from './Tooltip.jsx';
* - [{ icon: Component, label: string, onClick: (item) => void, variant?: string }]
* @param {boolean} props.selectable - Включить чекбоксы выбора (default: true)
* @param {number} props.skeletonRows - Количество строк скелетона (default: 10)
+ * @param {boolean} props.striped - Чередование фона строк (Tabler UX, default: true)
+ * @param {boolean} props.compact - Уменьшенные отступы ячеек (default: false)
*/
function DataTable({
columns = [],
@@ -56,11 +58,13 @@ function DataTable({
actions = [],
selectable = true,
skeletonRows = 10,
+ striped = true,
+ compact = false,
}) {
const hasActions = onEdit || onDelete || onCopy || actions.length > 0;
const isEditing = (item) => inlineEdit && item[itemKey] === inlineEdit.editingKey;
- // Рендер заголовка колонки
+ // Рендер заголовка колонки (col.title — подпись, col.tooltip — подсказка для UX)
const renderColumnHeader = (col) => {
const isSortable = col.sortable !== false && onSort;
const isActive = sortField === col.key;
@@ -77,22 +81,27 @@ function DataTable({
);
+ const thProps = {
+ key: col.key,
+ scope: 'col',
+ style: col.width ? { width: col.width } : undefined,
+ };
+
if (isSortable) {
return (
- | onSort(col.key)}
- style={col.width ? { width: col.width } : undefined}
>
- {content}
+ {col.tooltip ? {content} : content}
|
);
}
return (
-
- {content}
+ |
+ {col.tooltip ? {content} : content}
|
);
};
@@ -210,15 +219,20 @@ function DataTable({
}
const allSelected = items.length > 0 && items.every(i => selectedItems.has(i[itemKey]));
-
+ const tableClass = [
+ 'table card-table table-vcenter table-nowrap mb-0',
+ striped && 'table-striped',
+ compact && 'table-sm',
+ ].filter(Boolean).join(' ');
+
return (
<>
-
+
{selectable && (
- |
+ |
)}
{columns.map(renderColumnHeader)}
- {hasActions && | Действия | }
+ {hasActions && Действия | }
@@ -261,7 +275,7 @@ function DataTable({
))}
{hasActions && (
-
+ |
{renderActions(item)}
|
)}
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 3eef7bd..c356da9 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -296,7 +296,7 @@ button:focus-visible,
z-index: 2;
}
-/* ===== Table Improvements ===== */
+/* ===== Table Improvements (Tabler + UX) ===== */
.table thead th {
font-weight: 600;
text-transform: uppercase;
@@ -318,6 +318,20 @@ button:focus-visible,
background-color: rgba(32, 107, 196, 0.1);
}
+/* Колонка «Действия» — фиксированная ширина, не сжимается (лаконичный UX) */
+.table .td-actions {
+ min-width: 140px;
+ white-space: nowrap;
+}
+
+/* Выбранная строка поверх zebra (table-striped) */
+.table-striped tbody tr.table-selected > * {
+ --tblr-table-bg-type: rgba(32, 107, 196, 0.08);
+}
+.table-striped tbody tr.table-selected:hover > * {
+ --tblr-table-bg-type: rgba(32, 107, 196, 0.12);
+}
+
.user-select-none {
user-select: none;
-webkit-user-select: none;