feat(channels): фон комнаты и статичные превью при reduced-motion

- миграция 00014: background_file_id у комнаты (AGENT.md 7.5, 7.7)
- API: POST/DELETE /channels/{id}/background под MANAGE_CHANNEL_BACKGROUND,
  фон отдаётся в списке комнат и рассылается событием CHANNEL_UPDATE
- клиент: фон рисуется за лентой с затемнением, блок «Фон комнаты» в настройках
  комнаты (загрузка, замена, удаление)
- AnimatedImage: при «уменьшить движение» показывает статичный первый кадр,
  нарисованный на canvas, вместо анимации
- тесты: Go (загрузка, права, снятие) и Vitest (лента, настройки, reduced-motion)
This commit is contained in:
2026-09-21 18:08:21 +03:00
parent 91f415e36d
commit 0f0060e8a4
19 changed files with 733 additions and 16 deletions
+29
View File
@@ -0,0 +1,29 @@
import { useReducedMotion, useStaticFrame } from '@/lib/reducedMotion';
/**
* Картинка, которая при «уменьшить движение» показывает статичный первый кадр
* вместо анимации (AGENT.md 7.2, 7.7). Используется для фонов, рамок, аватаров
* и баннеров — везде, где может лежать GIF или анимированный WebP.
*/
interface AnimatedImageProps {
src: string;
alt?: string;
className?: string;
loading?: 'lazy' | 'eager';
}
/** Картинка, уважающая настройку «уменьшить движение». */
export function AnimatedImage({ src, alt = '', className, loading }: AnimatedImageProps) {
const reduced = useReducedMotion();
const frame = useStaticFrame(src, reduced);
return (
<img
src={frame ?? src}
alt={alt}
{...(className === undefined ? {} : { className })}
{...(loading === undefined ? {} : { loading })}
data-animated={frame !== undefined && frame !== src ? 'static' : 'animated'}
/>
);
}