Files
mdm-ordinis/ordinis-admin-ui/src/ui/components/table.tsx
T
Zimin A.N. 0acba4c073 feat(handoff): WorkflowBanner + table a11y + responsive + toast styling
Closing handoff gaps выявленных при полном audit'е README.md vs current
implementation. См. screen-by-screen mapping ниже.

== Screen 2 — Editor: WorkflowBanner (NEW) ==
Required by handoff (line 238-242) но не существовал. Inline status row выше
tab bar — live/review/info variants. Backend не имеет explicit dict-level
workflow state (draft → review → live transition), используем proxy:
  - approvalRequired=false      → 'Live' (green-bg + green left edge)
  - approvalRequired=true + 0   → 'Approval enabled' (accent-bg + accent edge)
  - approvalRequired=true + N   → 'На ревью N' (warn-bg + warn edge), link к /reviews

Backend extension future: добавить explicit workflowState к DictionaryDetail
+ buttons для transitions (request-review / approve / publish). Сейчас banner
дает минимум — visual workflow signal без UI mutation buttons.

== Table accessibility per handoff line 476 ==
TableHeaderCell:
  - scope='col' (a11y screen reader requirement)
  - aria-sort='ascending|descending|none' когда sortable prop true
  - New sortable + sortDirection props (API ready, не используется пока)

== 560px responsive per handoff line 466 ==
TableHeaderCell + TableCell получили optional hideBelow='sm|md|lg' prop:
  hideBelow='sm' → hidden <640px (table-cell ≥sm)
  hideBelow='md' → hidden <768px
  hideBelow='lg' → hidden <1024px

Callsites могут пометить secondary columns (created_by, updated_at, etc.)
hideBelow='md' для clean mobile experience. Текущие routes ещё не используют
prop — API ready для incremental adoption.

== Toast styling per handoff line 302-305 ==
Sonner toast — раньше surface bg + ink text. Handoff:
  - Navy bg + on-accent text (warm cream-orange feeling)
  - 4s auto-dismiss
  - Variant accent через left border 4px:
    success → green / error → pink / info → accent / warning → warn
  - cap-style title (но используем text-body для нативности, не overkill)
  - 13px font-sans

richColors disabled — наши tokens вместо sonner defaults.

Files:
  - NEW src/components/editor/WorkflowBanner.tsx
  - src/routes/dictionaries.$name.tsx (wire banner above tab bar)
  - src/ui/components/table.tsx (a11y + hideBelow API)
  - src/ui/components/toaster.tsx (navy variant + variant borders)
  - src/ui/index.ts (TableCellProps export)

Tests: 116 pass. TS strict clean.
2026-05-11 15:58:12 +03:00

197 lines
5.6 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'
/** Hide column на narrow viewports per handoff line 466:
* - 'sm' — hide <640px (table cells 6-7 columns secondary)
* - 'md' — hide <768px
* - 'lg' — hide <1024px
* Используется pair с same prop на соответствующих TableCell. */
hideBelow?: 'sm' | 'md' | 'lg'
sortable?: boolean
sortDirection?: 'asc' | 'desc' | 'none'
}
const HIDE_BELOW: Record<NonNullable<TableHeaderCellProps['hideBelow']>, string> = {
sm: 'hidden sm:table-cell',
md: 'hidden md:table-cell',
lg: 'hidden lg:table-cell',
}
export const TableHeaderCell = React.forwardRef<HTMLTableCellElement, TableHeaderCellProps>(
function TableHeaderCell(
{ className, align = 'left', hideBelow, sortable, sortDirection, ...props },
ref,
) {
const ariaSort =
sortDirection === 'asc'
? 'ascending'
: sortDirection === 'desc'
? 'descending'
: sortable
? 'none'
: undefined
return (
<th
ref={ref}
scope="col"
aria-sort={ariaSort}
className={cn(
'h-9 px-3 align-middle text-cap text-mute',
align === 'right' && 'text-right',
align === 'center' && 'text-center',
align === 'left' && 'text-left',
hideBelow && HIDE_BELOW[hideBelow],
'[&:has([role=checkbox])]:pr-0',
className,
)}
{...props}
/>
)
},
)
export type TableCellProps = React.TdHTMLAttributes<HTMLTableCellElement> & {
/** Pair с TableHeaderCell.hideBelow — column скрывается на narrow viewport. */
hideBelow?: 'sm' | 'md' | 'lg'
}
export const TableCell = React.forwardRef<HTMLTableCellElement, TableCellProps>(
function TableCell({ className, hideBelow, ...props }, ref) {
return (
<td
ref={ref}
className={cn(
'px-3 py-2 align-middle',
hideBelow && HIDE_BELOW[hideBelow],
'[&: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>
)
}