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.
This commit is contained in:
Zimin A.N.
2026-05-11 15:58:12 +03:00
parent 787cea9db1
commit 0acba4c073
5 changed files with 217 additions and 29 deletions
+50 -13
View File
@@ -89,18 +89,46 @@ export const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttribut
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', ...props }, ref) {
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,
)}
@@ -110,18 +138,27 @@ export const TableHeaderCell = React.forwardRef<HTMLTableCellElement, TableHeade
},
)
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 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,