refactor(DataTable): enhance table styling and functionality with fixed action column, improved striped rows, and new props for UX customization
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m52s

This commit is contained in:
2026-02-16 15:56:28 +07:00
parent 1fe9cf4794
commit af8af84ab7
3 changed files with 84 additions and 13 deletions
+43
View File
@@ -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` вокруг `<table>`
- **Обрезка длинного текста:** для ячейки использовать `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` и общих обработчиков.
+26 -12
View File
@@ -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({
</div>
);
const thProps = {
key: col.key,
scope: 'col',
style: col.width ? { width: col.width } : undefined,
};
if (isSortable) {
return (
<th
key={col.key}
<th
{...thProps}
className="cursor-pointer user-select-none"
onClick={() => onSort(col.key)}
style={col.width ? { width: col.width } : undefined}
>
{content}
{col.tooltip ? <Tooltip content={col.tooltip} position="top">{content}</Tooltip> : content}
</th>
);
}
return (
<th key={col.key} style={col.width ? { width: col.width } : undefined}>
{content}
<th {...thProps}>
{col.tooltip ? <Tooltip content={col.tooltip} position="top">{content}</Tooltip> : content}
</th>
);
};
@@ -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 (
<>
<div className="table-responsive">
<table className="table card-table table-vcenter table-nowrap mb-0">
<table className={tableClass} role="grid">
<thead>
<tr>
{selectable && (
<th style={{ width: '40px' }}>
<th scope="col" style={{ width: '40px' }}>
<input
type="checkbox"
className="form-check-input"
@@ -229,7 +243,7 @@ function DataTable({
</th>
)}
{columns.map(renderColumnHeader)}
{hasActions && <th className="text-end">Действия</th>}
{hasActions && <th scope="col" className="text-end td-actions">Действия</th>}
</tr>
</thead>
<tbody>
@@ -261,7 +275,7 @@ function DataTable({
</td>
))}
{hasActions && (
<td className="text-end">
<td className="text-end td-actions">
{renderActions(item)}
</td>
)}
+15 -1
View File
@@ -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;