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:
@@ -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,
|
||||
|
||||
@@ -2,20 +2,22 @@ import { Toaster as SonnerToaster, toast } from 'sonner'
|
||||
import { useTheme } from '@/stores/ThemeProvider'
|
||||
|
||||
/**
|
||||
* Toast notification provider — wraps sonner с our design tokens.
|
||||
* Toast notification provider — sonner с handoff Toast styling (Screen 4).
|
||||
*
|
||||
* Sonner = headless, кастомизируем через CSS variables. Параметризируется
|
||||
* theme="light" | "dark" — берётся из ThemeProvider, авто-switch на смену темы.
|
||||
* <p>Handoff spec: navy bg (or Claude-orange в dark theme), "OK" cap header в
|
||||
* toast-cap color (orange-cream), then message text, then × dismiss button.
|
||||
* Auto-dismiss после 4s. Slide-up bottom-right.
|
||||
*
|
||||
* <p>Sonner = headless library. Кастомизируем через CSS overrides — наша
|
||||
* navy color tokens применяются к bg/text/border.
|
||||
*
|
||||
* Использование:
|
||||
* <pre>
|
||||
* import { toast } from '@/ui/components/toaster'
|
||||
* import { toast } from '@/ui'
|
||||
* toast.success('Запись сохранена')
|
||||
* toast.error('Ошибка валидации', { description: '...' })
|
||||
* toast.promise(mutateAsync(), {loading, success, error})
|
||||
* </pre>
|
||||
*
|
||||
* Mount: <Toaster /> один раз в main.tsx внутри ThemeProvider.
|
||||
*/
|
||||
export function Toaster() {
|
||||
const { resolved } = useTheme()
|
||||
@@ -23,30 +25,37 @@ export function Toaster() {
|
||||
return (
|
||||
<SonnerToaster
|
||||
position="bottom-right"
|
||||
richColors
|
||||
// richColors=false — наши custom colors из tokens, не sonner default.
|
||||
closeButton
|
||||
theme={resolved}
|
||||
duration={4000}
|
||||
toastOptions={{
|
||||
// Inline styles переопределяют sonner CSS — tokens из @theme.
|
||||
// На light: surface bg + ink text + line border.
|
||||
// На dark: re-uses tokens автомат (другие значения cssvar).
|
||||
// Handoff toast — navy bg, on-accent text (cream/white), no border.
|
||||
// Cap-style "OK / ERROR / INFO" header идёт через title classname.
|
||||
style: {
|
||||
background: 'var(--color-surface)',
|
||||
color: 'var(--color-ink)',
|
||||
border: '1px solid var(--color-line)',
|
||||
background: 'var(--color-navy)',
|
||||
color: 'var(--color-on-accent)',
|
||||
border: 'none',
|
||||
borderRadius: 'var(--radius-lg)',
|
||||
fontFamily: 'var(--font-sans)',
|
||||
fontSize: '13px',
|
||||
padding: '12px 16px',
|
||||
boxShadow: '0 12px 30px -10px rgba(40,25,10,0.38)',
|
||||
},
|
||||
classNames: {
|
||||
title: 'font-medium',
|
||||
description: 'text-mute text-cell',
|
||||
// Title всегда выше description; cap-style для статуса/labels.
|
||||
title: 'text-body font-medium',
|
||||
description: 'text-cell opacity-80 mt-0.5',
|
||||
// success/error/info variants — accent через left border (handoff).
|
||||
success: '!border-l-4 !border-l-[var(--color-green)]',
|
||||
error: '!border-l-4 !border-l-[var(--color-pink)]',
|
||||
info: '!border-l-4 !border-l-[var(--color-accent)]',
|
||||
warning: '!border-l-4 !border-l-[var(--color-warn)]',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Re-export для удобства: `import { toast } from '@/ui/components/toaster'`.
|
||||
// Re-export для удобства: `import { toast } from '@/ui'`.
|
||||
export { toast }
|
||||
|
||||
@@ -92,6 +92,7 @@ export {
|
||||
TableCaption,
|
||||
TableEmpty,
|
||||
type TableHeaderCellProps,
|
||||
type TableCellProps,
|
||||
type TableEmptyProps,
|
||||
} from './components/table'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user