Display

Aspect Video Frame

Рамка встраивания с фиксированным соотношением сторон: место под ролик занято до того, как iframe успел загрузиться, поэтому страница не прыгает.

  • frame
  • video
  • embed
  • aspect-ratio

Превью

1440pxHost theme

Использовать с AI

  1. 1. Скопируйте ссылку.
  2. 2. Напишите агенту своими словами и вставьте её в предложение.
  3. 3. Агент откроет ссылку и поставит компонент из registry.

размести это в шапке: https://vibeui.ru/c/frame-002

Обзор каталога VibeUI
16:9
Ролик подгружается — место под него уже занято
Показать полную инструкцию

То, что лежит по ссылке в развёрнутом виде. Нужна, если агент не может открыть ссылку — тогда вставьте этот текст целиком.

# Установка и размещение «frame-002» (Aspect Video Frame) из VibeUI

## 1. Сначала установка — не пропускай, не пересоздавай
Выполни эту команду до того, как писать код:
npx shadcn@latest add https://vibeui.ru/r/frame-002.json

Registry item: https://vibeui.ru/r/frame-002.json
Ставится в: components/vibeui/frame-002.tsx (точный путь следует алиасам components.json этого проекта).
npm-зависимости: нет.
Registry-зависимости: нет.

Ставь из реестра. Не пересоздавай по описанию, не подменяй похожим
компонентом из другой библиотеки и не переписывай под стиль проекта.

## 2. Что это
Рамка встраивания с фиксированным соотношением сторон: место под ролик занято до того, как iframe успел загрузиться, поэтому страница не прыгает.

Рамка встраивания видео: соотношение сторон резервирует место до загрузки, вложенный iframe растягивается на всю рамку. Ноль зависимостей, один файл, клиентского JS нет.

## 3. Как использовать
import { Frame002 } from "@/components/vibeui/frame-002"

<Frame002 ratio="16:9" title="Обзор каталога VibeUI">
  <iframe src="https://example.com/embed/abc" title="Обзор" />
</Frame002>

Полный список пропсов — в установленном файле.

## 4. Куда поставить
Это мелкий inline-компонент. Поставь его ровно туда, куда просил
пользователь, внутрь существующей разметки. Не создавай ради него
новую страницу, секцию или обёртку. Если на этом месте уже стоит похожий
контрол — замени его, а не добавляй второй.

Куда поставить: ___
(Эту строку заполняет пользователь. Если она осталась пустой — спроси,
а не угадывай.)

## 5. Сохранить как установлено
- локальную палитру --vibeui-frame-002-* и не подменять её токенами вашей темы
- соотношение сторон на самой рамке: без него страница прыгает в момент загрузки ролика
- растягивание вложенного iframe на inset:0 — иначе он остаётся высотой 150px и ломает пропорцию
- overflow:hidden на рамке: у видеоплееров свои скруглённые углы, которые вылезают за границу
- заглушку с треугольником: пустой тёмный прямоугольник читается как ошибка загрузки
- собственную светлую подложку: тёмный экран без неё сливается с тёмным фоном каталога

## 6. Можно менять
- пропорцию через ratio: 16:9 для роликов, 1:1 для сторис, 21:9 для трейлеров
- подпись заглушки через title
- текст под рамкой через caption
- ширину рамки через max-width

## 7. Правила
- Соотношение сторон поддерживается всеми актуальными браузерами, но в очень старых движках рамка схлопнется в ноль высоты.
- object-fit:cover обрезает картинку по краям: если кадр важен целиком, поменяйте на contain.
- Внутрь допустим ровно один медиа-узел — двум абсолютным детям рамка не поможет.
- Не выносите CSS-переменные в globals.css: компонент обязан оставаться одним файлом.

## 8. Проверка
- рендерится без ошибок в консоли;
- выглядит так же, как превью на странице VibeUI, откуда взята ссылка;
- если нет — ты изменил что-то из раздела 5, верни как было.

Для разработчика

npx shadcn@latest add https://vibeui.ru/r/frame-002.json
https://vibeui.ru/r/frame-002.json

Серверный компонент, хуков нет. Соотношение сторон объявлено переменной --vibeui-frame-002-ratio на самой рамке, а варианты 4:3, 1:1 и 21:9 переопределяют её через атрибут data-ratio — так пропорция задаётся одной строкой, а не набором классов. Вложенный iframe, img или video растягиваются правилом для :where(iframe,img,video) на inset:0: без него iframe держит собственные 150px высоты и пропорция рамки перестаёт что-либо значить. Заглушка с треугольником нарисована рамками через ::before, чтобы не тащить svg. Компонент несёт свою белую подложку — тёмный экран внутри белой карточки читается на любом фоне каталога.

Исходник компонента

Тот же файл, который поставит агент. Нужен, если вы предпочитаете скопировать код руками.

import type { ComponentPropsWithoutRef, CSSProperties, ReactNode } from "react"

export type Frame002Props = Omit<
  ComponentPropsWithoutRef<"figure">,
  "title"
> & {
  ratio?: "16:9" | "4:3" | "1:1" | "21:9"
  title?: string
  caption?: string
  children?: ReactNode
}

// Идея компонента: рамка встраивания держит пропорцию до загрузки контента.
// Место под ролик резервируется соотношением сторон на самой рамке, поэтому
// страница не прыгает, когда iframe наконец приходит. Вложенный iframe
// растягивается абсолютом: без этого он оставляет свои дефолтные 150px
// высоты и пропорция рамки перестаёт что-либо значить.
const STYLES = `
:where([data-vibeui-block="frame-002"]){
--vibeui-frame-002-ratio:16 / 9;
--vibeui-frame-002-bg:oklch(1 0 0);
--vibeui-frame-002-screen:oklch(0.22 0.02 265);
--vibeui-frame-002-fg:oklch(0.24 0.014 265);
--vibeui-frame-002-muted:oklch(0.56 0.014 265);
--vibeui-frame-002-border:oklch(0.9 0.006 265);
--vibeui-frame-002-light:oklch(0.98 0.002 265);
--vibeui-frame-002-radius:0.75rem;
--vibeui-frame-002-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="frame-002"]{
display:flex;flex-direction:column;gap:0.625rem;margin:0;
width:100%;max-width:32rem;box-sizing:border-box;padding:0.625rem;
background:var(--vibeui-frame-002-bg);
border:1px solid var(--vibeui-frame-002-border);
border-radius:calc(var(--vibeui-frame-002-radius) + 0.25rem);
font-family:var(--vibeui-frame-002-font);color:var(--vibeui-frame-002-fg);
}
[data-vibeui-block="frame-002"] *{box-sizing:border-box}
/* Соотношение сторон живёт на рамке: место под ролик занято до загрузки. */
[data-vibeui-block="frame-002"] [data-part="shell"]{
position:relative;overflow:hidden;
aspect-ratio:var(--vibeui-frame-002-ratio);
border-radius:var(--vibeui-frame-002-radius);
background:var(--vibeui-frame-002-screen);
}
/* Встраиваемый узел растягивается абсолютом: iframe иначе держит свои 150px. */
[data-vibeui-block="frame-002"] [data-part="shell"] > :where(iframe,img,video){
position:absolute;inset:0;display:block;width:100%;height:100%;border:0;
object-fit:cover;
}
[data-vibeui-block="frame-002"] [data-part="stub"]{
position:absolute;inset:0;
display:grid;place-content:center;justify-items:center;gap:0.625rem;
padding:1rem;text-align:center;color:var(--vibeui-frame-002-light);
}
[data-vibeui-block="frame-002"] [data-part="play"]{
display:grid;place-items:center;
width:3rem;height:3rem;border-radius:9999px;
background:color-mix(in oklab,var(--vibeui-frame-002-light) 16%,transparent);
border:1px solid color-mix(in oklab,var(--vibeui-frame-002-light) 40%,transparent);
}
[data-vibeui-block="frame-002"] [data-part="play"]::before{
content:"";width:0;height:0;margin-left:0.2rem;
border-left:0.75rem solid var(--vibeui-frame-002-light);
border-top:0.5rem solid transparent;border-bottom:0.5rem solid transparent;
}
[data-vibeui-block="frame-002"] [data-part="stub-title"]{font-size:0.8125rem;opacity:.85}
[data-vibeui-block="frame-002"] [data-part="badge"]{
position:absolute;right:0.5rem;bottom:0.5rem;
padding:0.125rem 0.375rem;border-radius:0.375rem;
background:color-mix(in oklab,var(--vibeui-frame-002-screen) 65%,transparent);
color:var(--vibeui-frame-002-light);
font-size:0.6875rem;font-variant-numeric:tabular-nums;
}
[data-vibeui-block="frame-002"] figcaption{
font-size:0.75rem;line-height:1.4;color:var(--vibeui-frame-002-muted);
}
[data-vibeui-block="frame-002"][data-ratio="4:3"]{--vibeui-frame-002-ratio:4 / 3}
[data-vibeui-block="frame-002"][data-ratio="1:1"]{--vibeui-frame-002-ratio:1 / 1}
[data-vibeui-block="frame-002"][data-ratio="21:9"]{--vibeui-frame-002-ratio:21 / 9}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="frame-002"] *{animation:none!important;transition:none!important}}
`

/**
 * Рамка встраивания с фиксированным соотношением сторон.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Frame002({
  ratio = "16:9",
  title = "Обзор каталога VibeUI",
  caption = "Ролик подгружается — место под него уже занято",
  children,
  className,
  style,
  ...props
}: Frame002Props) {
  return (
    <>
      <style href="vibeui-frame-002" precedence="medium">
        {STYLES}
      </style>
      <figure
        {...props}
        data-vibeui-block="frame-002"
        data-ratio={ratio}
        className={className}
        style={style as CSSProperties}
      >
        <div data-part="shell">
          {children ?? (
            <div data-part="stub">
              <span data-part="play" aria-hidden="true" />
              <span data-part="stub-title">{title}</span>
            </div>
          )}
          <span data-part="badge">{ratio}</span>
        </div>
        {caption ? <figcaption>{caption}</figcaption> : null}
      </figure>
    </>
  )
}