787cea9db1
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.
160 lines
4.5 KiB
TypeScript
160 lines
4.5 KiB
TypeScript
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). Применяется на
|
||
* <td/th> через 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>
|
||
)
|
||
}
|