Files
mdm-ordinis/ordinis-admin-ui/src/ui/components/table.tsx
T
Zimin A.N. 787cea9db1 fix(table+layout): TableHead alias to THEAD wrapper + kill horizontal scroll
CRITICAL bugs reported by user — table выглядела кривой, контент уезжал
за экран с горизонтальным скроллом.

Bug 1 — invalid HTML в Table:
  Callsites используют <TableHead><TableRow><TableHeaderCell>... ожидая
  нstart-style THEAD wrapper. Но мой alias делал TableHead = TableHeaderCell
  (TH cell). Получалось <th><tr><th>...</th></tr></th> — invalid nested,
  browser parser hoist'ил TableRow наружу, создавая phantom 509px column
  слева от данных. Колонки выглядели misaligned, content смещён вправо.

  Fix: TableHead → THEAD wrapper (alias TableHeader). TableHeaderCell
  остаётся отдельным TH cell. Source semantically correct, browser parses
  валидно, phantom column исчез.

Bug 2 — horizontal overflow:
  Scope strip border на верху detail-страницы использовал
  '-mt-6 -mx-4 sm:-mx-6 lg:-mx-8'. На lg viewport main padding только
  sm:px-6 (24px), strip negative margins -mx-8 (32px) → 8px overflow each
  side → body.scrollWidth > viewport → horizontal scrollbar.

  Fix:
  - Strip negative margins только до -mx-6 (соответствует main padding)
  - main получил overflow-x-clip + max-w-7xl wrapper получил min-w-0
    (защита от любых future overflow children)

Verified в preview viewport 1440 + 375: body.scrollWidth === viewport,
no h-scroll.
2026-05-11 15:49:27 +03:00

160 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as React from 'react'
import { cn } from '@/lib/utils'
/**
* Table primitives — shadcn-style atomic wrappers. Замена @nstart/ui Table*.
*
* <p>Per handoff: table cell text = text-cell (12.5/500). Применяется на
* &lt;td/th&gt; через CSS rule в `<tbody>` — но проще baked-in via TableCell
* className. Caller может override через className prop.
*
* <p>TableEmpty — single full-width row для empty state.
*/
export const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
function Table({ className, ...props }, ref) {
return (
<div className="w-full overflow-x-auto">
<table
ref={ref}
className={cn('w-full caption-bottom text-cell', className)}
{...props}
/>
</div>
)
},
)
export const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(function TableHeader({ className, ...props }, ref) {
return (
<thead
ref={ref}
className={cn('[&_tr]:border-b [&_tr]:border-line', className)}
{...props}
/>
)
})
// nstart-compat alias: nstart использовал <TableHead> как THEAD wrapper.
// shadcn по convention использует TableHead как TH cell — это конфликтовало
// (callsites писали `<TableHead><TableRow>...` ожидая THEAD-семантику).
// Резолвим в пользу nstart-compat (callsites больше). Для TH cell — TableHeaderCell.
export const TableHead = TableHeader
export const TableBody = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(function TableBody({ className, ...props }, ref) {
return (
<tbody
ref={ref}
className={cn('[&_tr:last-child]:border-0', className)}
{...props}
/>
)
})
export const TableFooter = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(function TableFooter({ className, ...props }, ref) {
return (
<tfoot
ref={ref}
className={cn(
'border-t border-line bg-surface-2 font-medium [&>tr]:last:border-b-0',
className,
)}
{...props}
/>
)
})
export const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
function TableRow({ className, ...props }, ref) {
return (
<tr
ref={ref}
className={cn(
'border-b border-line transition-colors hover:bg-surface-2/40 data-[state=selected]:bg-accent-bg',
className,
)}
{...props}
/>
)
},
)
export type TableHeaderCellProps = React.ThHTMLAttributes<HTMLTableCellElement> & {
align?: 'left' | 'center' | 'right'
}
export const TableHeaderCell = React.forwardRef<HTMLTableCellElement, TableHeaderCellProps>(
function TableHeaderCell({ className, align = 'left', ...props }, ref) {
return (
<th
ref={ref}
className={cn(
'h-9 px-3 align-middle text-cap text-mute',
align === 'right' && 'text-right',
align === 'center' && 'text-center',
align === 'left' && 'text-left',
'[&:has([role=checkbox])]:pr-0',
className,
)}
{...props}
/>
)
},
)
export const TableCell = React.forwardRef<
HTMLTableCellElement,
React.TdHTMLAttributes<HTMLTableCellElement>
>(function TableCell({ className, ...props }, ref) {
return (
<td
ref={ref}
className={cn('px-3 py-2 align-middle [&:has([role=checkbox])]:pr-0', className)}
{...props}
/>
)
})
export const TableCaption = React.forwardRef<
HTMLTableCaptionElement,
React.HTMLAttributes<HTMLTableCaptionElement>
>(function TableCaption({ className, ...props }, ref) {
return (
<caption
ref={ref}
className={cn('mt-3 text-cell text-mute', className)}
{...props}
/>
)
})
/**
* TableEmpty — full-width row с заглушкой "ничего не найдено".
* Замена @nstart/ui TableEmptyRow API: <TableEmpty colSpan={N}>{text}</TableEmpty>.
*/
export type TableEmptyProps = React.TdHTMLAttributes<HTMLTableCellElement> & {
colSpan: number
}
export function TableEmpty({ colSpan, className, children, ...props }: TableEmptyProps) {
return (
<tr>
<td
colSpan={colSpan}
className={cn('py-10 text-center text-cell text-mute', className)}
{...props}
>
{children}
</td>
</tr>
)
}