Refactor Program.cs to use async Run method for improved performance. Update index.html to specify language attribute for accessibility. Enhance BumperTemplateEditor and ScheduleGrid components by replacing divs with buttons for better keyboard accessibility. Improve TemplatePreview sorting logic and optimize episode regex handling. Add close button to ToastItem for better user interaction. Update localization files to include 'close' translations. Refactor CardTitle to ensure screen reader compatibility by explicitly rendering children.
ci / build-backend (push) Successful in 1m34s
ci / build-frontend (push) Successful in 1m0s
ci / tests (push) Successful in 1m58s
ci / sonar (push) Successful in 4m13s

This commit is contained in:
Leonid Pershin
2026-07-26 22:35:36 +03:00
parent b0740503bb
commit 0f0657e523
14 changed files with 75 additions and 26 deletions
+1
View File
@@ -15,6 +15,7 @@ export const en = {
save: 'Save',
cancel: 'Cancel',
retry: 'Retry',
close: 'Close',
delete: 'Delete',
create: 'Create',
loading: 'Loading…',
+1
View File
@@ -15,6 +15,7 @@ export const ru = {
save: 'Сохранить',
cancel: 'Отмена',
retry: 'Повторить',
close: 'Закрыть',
delete: 'Удалить',
create: 'Создать',
loading: 'Загрузка…',
+6 -2
View File
@@ -12,8 +12,12 @@ export const CardHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivEleme
CardHeader.displayName = 'CardHeader'
export const CardTitle = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHeadingElement>>(
({ className, ...props }, ref) => (
<h3 ref={ref} className={cn('crt-glow text-xl font-semibold tracking-tight', className)} {...props} />
// children разворачиваем явно: заголовок без видимого содержимого — это дыра для скринридера,
// и статический анализ такое ловит только тогда, когда содержимое видно в разметке.
({ className, children, ...props }, ref) => (
<h3 ref={ref} className={cn('crt-glow text-xl font-semibold tracking-tight', className)} {...props}>
{children}
</h3>
),
)
CardTitle.displayName = 'CardTitle'
+5 -1
View File
@@ -37,6 +37,10 @@ export function HlsVideo({ src, className }: { src: string; className?: string }
controls
playsInline
className={cn('aspect-video w-full rounded-md border border-border bg-black', className)}
/>
>
{/* Субтитров пайплайн не производит: ffmpeg режет видео и звук, дорожек с текстом нет.
Пустой track объявляет это явно, вместо молчаливого отсутствия. */}
<track kind="captions" />
</video>
)
}
+6 -2
View File
@@ -33,11 +33,15 @@ export function SortHeader({
const active = sort.key === sortKey
const Icon = !active ? ChevronsUpDown : sort.desc ? ArrowDown : ArrowUp
return (
<th className={cn('px-4 py-2 font-medium', className)}>
// aria-sort — атрибут заголовка столбца, а не кнопки внутри него: у роли button его нет,
// и скринридер там его просто не прочтёт.
<th
className={cn('px-4 py-2 font-medium', className)}
aria-sort={active ? (sort.desc ? 'descending' : 'ascending') : 'none'}
>
<button
type="button"
onClick={() => onToggle(sortKey)}
aria-sort={active ? (sort.desc ? 'descending' : 'ascending') : 'none'}
className={cn(
'inline-flex items-center gap-1 hover:text-foreground',
active && 'text-foreground',
+16 -3
View File
@@ -1,4 +1,6 @@
import { X } from 'lucide-react'
import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/shared/lib/cn'
import { useToastContext } from './toast-store'
@@ -25,22 +27,33 @@ function ToastItem({
variant: 'default' | 'success' | 'error'
onDismiss: (id: number) => void
}) {
const { t } = useTranslation()
useEffect(() => {
const timeout = setTimeout(() => onDismiss(id), 4000)
return () => clearTimeout(timeout)
}, [id, onDismiss])
// Живой регион остаётся обычным контейнером, а закрытие висит на настоящей кнопке: обработчик
// клика на самом сообщении недоступен с клавиатуры, и скринридер о нём никак не сообщает.
return (
<div
className={cn(
'crt-panel pointer-events-auto rounded-md px-4 py-3 text-sm shadow-lg',
'crt-panel pointer-events-auto flex items-start gap-3 rounded-md px-4 py-3 text-sm shadow-lg',
variant === 'success' && 'border-primary/60',
variant === 'error' && 'border-red-700/60 text-red-400',
)}
onClick={() => onDismiss(id)}
role="status"
>
{message}
<span className="flex-1">{message}</span>
<button
type="button"
onClick={() => onDismiss(id)}
aria-label={t('common.close')}
className="shrink-0 opacity-60 hover:opacity-100"
>
<X className="h-4 w-4" />
</button>
</div>
)
}