Hero

Console Hero

Светлый B2B SaaS hero со split-раскладкой: слева текст и пара CTA, справа декоративная визуализация продукта. Самодостаточен: не зависит от темы и стиля проекта.

  • light
  • saas
  • split
  • dashboard
  • b2b
  • landing

Превью

1440pxHost theme

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

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

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

# Установка и размещение «hero-002» (Console Hero) из VibeUI

## 1. Сначала установка — не пропускай, не пересоздавай
Команда установки недоступна: URL реестра VibeUI не сконфигурирован.

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

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

## 2. Что это
Светлый B2B SaaS hero со split-раскладкой: слева текст и пара CTA, справа декоративная визуализация продукта. Самодостаточен: не зависит от темы и стиля проекта.

Светлый B2B SaaS-hero со split-композицией. Слева: eyebrow-pill, крупный заголовок в две части, описание, две CTA-ссылки и trust-строка с декоративной стопкой аватаров. Справа: декоративное окно приложения — шапка с точками, боковой rail, две плитки метрик, area-график на inline SVG, лента активности и floating-чип. Ноль зависимостей, один файл, раскладка и типографика считаются от ширины блока через container-запросы и cqi.

## 3. Как использовать
Это целая секция страницы. Вставляй её одним куском — не вынимай из
неё фрагменты и не собирай заново из мелких компонентов.
Имя экспорта и пропсы смотри в установленном файле.

## 4. Куда поставить
Это полноширинная секция страницы. Поставь её прямым потомком
разметки страницы, в том порядке, который назвал пользователь. Не
вкладывай внутрь карточки, сайдбара, модального окна или другого
узкого контейнера: секция меряет собственную ширину и разложится
неправильно.

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

## 5. Сохранить как установлено
- split-раскладку: контент слева, визуализация продукта справа от 64rem ширины блока
- сам product mockup — это часть дизайна блока, а не placeholder; не удалять, не заменять на <img>, скриншот, картинку или готовый dashboard-компонент проекта
- светлую локальную палитру --vibeui-hero-002-* — не заменять её на токены темы проекта (bg-background, text-foreground, primary, border и т.п.)
- container-type: inline-size на корне блока и @container-правила в его <style> — блок меряет собственную ширину, а не ширину окна; не переписывай их на viewport-медиазапросы
- типографику в единицах cqi, clamp и tracking
- area-график как inline SVG с фиксированным путём — это иллюстрация, а не данные
- оформление окна: рамка, радиус, мягкая тень, шапка с точками, боковой rail, структуру плиток метрик и floating-чип
- rounded-lg у CTA — этим блок отличается от hero-001 с его pill-кнопками; не менять на rounded-full
- одноразовые entrance-анимации и стагер; в этом блоке нет бесконечных анимаций
- блок <style> внутри компонента — в нём палитра, container-правила, keyframes и prefers-reduced-motion
- семантику: один h1, CTA как <a href>, весь mockup и декоративные слои aria-hidden

## 6. Можно менять
- тексты eyebrow, title, titleAccent, description, CTA, proof и panelTitle
- href у CTA
- label, value и delta у двух метрик
- акцентный цвет — проп accent (и accentForeground для контраста текста на кнопке)
- внешние отступы через className

## 7. Правила
- Не подключай chart-библиотеку (recharts, chart.js, visx и т.п.): график декоративный и уже собран на inline SVG.
- Не добавляй motion-библиотеки: анимации уже на CSS.
- Не выноси CSS-переменные в globals.css: блок должен оставаться одним файлом.
- Не заменяй визуализацию справа на реальный скриншот продукта или на существующий dashboard-компонент проекта — это сломает композицию блока.
- Если меняешь accent, проверь контраст текста на кнопке: нужно не менее 4.5:1.
- Сохраняй семантику ссылок. Для внутренней навигации можно использовать компонент роутера проекта, если сохраняются визуальные стили, доступность и поведение href.

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

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

Команда установки недоступна: переменная окруженияREGISTRY_BASE_URL

Блок самодостаточен: один файл, без зависимостей, всегда светлый независимо от темы проекта. Цвета живут в локальных переменных --vibeui-hero-002-*; акцент меняется пропом accent. Раскладка переключается container-запросами от ширины самого блока, поэтому он одинаково выглядит и на странице, и в миниатюре каталога. Визуализация справа собрана на HTML/CSS/inline SVG — внешних картинок нет.

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

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

import type { CSSProperties } from "react"

type Hero002Action = {
  label: string
  href: string
}

type Hero002Metric = {
  label: string
  value: string
  delta?: string
}

export type Hero002Props = {
  eyebrow?: string
  title?: string
  titleAccent?: string
  description?: string
  primaryAction?: Hero002Action
  secondaryAction?: Hero002Action
  proof?: string
  panelTitle?: string
  metrics?: Hero002Metric[]
  accent?: string
  accentForeground?: string
  className?: string
}

// Весь CSS блока живёт здесь, а не в globals.css проекта: палитра, раскладка,
// декоративные слои, тени и keyframes. Переменные объявлены в :where() с нулевой
// специфичностью, поэтому пользователь переопределяет их чем угодно.
//
// container-type делает блок собственным query-контейнером: раскладка и размер
// шрифта считаются от ширины блока, а не от ширины окна. Поэтому split виден и
// в миниатюре каталога, где блок рендерится в 1280px внутри узкого окна.
// Правила раскладки — плоским CSS, а не Tailwind-вариантами, чтобы блок не
// зависел от версии Tailwind в чужом проекте; специфичность (0,2,0) выше утилит.
const STYLES = `
:where([data-vibeui-block="hero-002"]){
--vibeui-hero-002-bg:oklch(0.981 0.004 255);
--vibeui-hero-002-panel:oklch(1 0 0);
--vibeui-hero-002-panel-alt:oklch(0.974 0.004 255);
--vibeui-hero-002-fg:oklch(0.21 0.02 260);
--vibeui-hero-002-muted:oklch(0.52 0.015 260);
--vibeui-hero-002-border:oklch(0.9 0.008 260);
--vibeui-hero-002-accent:oklch(0.48 0.17 262);
--vibeui-hero-002-accent-fg:oklch(0.99 0 0);
--vibeui-hero-002-positive:oklch(0.45 0.12 155);
--vibeui-hero-002-ring:color-mix(in oklab, var(--vibeui-hero-002-accent) 70%, transparent);
--vibeui-hero-002-shadow:color-mix(in oklab, var(--vibeui-hero-002-fg) 16%, transparent);
--vibeui-hero-002-dot:oklch(0.21 0.02 260 / 7%);
--vibeui-hero-002-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="hero-002"]{container-type:inline-size}
[data-vibeui-block="hero-002"] [data-part="glow"]{position:absolute;top:-33%;right:0;width:66%;height:66%;pointer-events:none;background:radial-gradient(50% 50% at 70% 60%,color-mix(in oklab,var(--vibeui-hero-002-accent) 14%,transparent),transparent 72%)}
[data-vibeui-block="hero-002"] [data-part="dots"]{position:absolute;inset:0;pointer-events:none;background-image:radial-gradient(var(--vibeui-hero-002-dot) 1px,transparent 1px);background-size:20px 20px;-webkit-mask-image:radial-gradient(80% 60% at 30% 40%,black,transparent 100%);mask-image:radial-gradient(80% 60% at 30% 40%,black,transparent 100%)}
[data-vibeui-block="hero-002"] [data-part="window"]{box-shadow:0 30px 60px -30px var(--vibeui-hero-002-shadow)}
[data-vibeui-block="hero-002"] [data-part="chip"]{box-shadow:0 12px 28px -16px var(--vibeui-hero-002-shadow)}
[data-vibeui-block="hero-002"] [data-part="cta-primary"]{box-shadow:0 8px 20px -10px color-mix(in oklab,var(--vibeui-hero-002-accent) 70%,transparent)}
@container (min-width:40rem){
[data-vibeui-block="hero-002"] [data-part="frame"]{min-height:600px;padding-left:2.5rem;padding-right:2.5rem}
[data-vibeui-block="hero-002"] [data-part="actions"]{width:auto;flex-direction:row}
[data-vibeui-block="hero-002"] [data-part="metrics"]{grid-template-columns:1fr 1fr}
[data-vibeui-block="hero-002"] [data-part="activity"]{display:grid}
[data-vibeui-block="hero-002"] [data-part="rail"]{display:flex}
}
@container (min-width:64rem){
[data-vibeui-block="hero-002"] [data-part="frame"]{min-height:680px;padding-top:5rem;padding-bottom:5rem}
[data-vibeui-block="hero-002"] [data-part="grid"]{grid-template-columns:minmax(0,0.9fr) minmax(0,1.1fr);align-items:center;gap:3.5rem}
}
@container (min-width:80rem){
[data-vibeui-block="hero-002"] [data-part="frame"]{min-height:720px;padding-left:4rem;padding-right:4rem}
[data-vibeui-block="hero-002"] [data-part="grid"]{max-width:1200px;margin-inline:auto;gap:4.5rem}
}
@keyframes vibeui-hero-002-fade-up{from{opacity:0;transform:translate3d(0,14px,0)}to{opacity:1;transform:none}}
@keyframes vibeui-hero-002-fade-in{from{opacity:0;transform:translate3d(16px,0,0) scale(0.985)}to{opacity:1;transform:none}}
@keyframes vibeui-hero-002-draw{from{stroke-dashoffset:420}to{stroke-dashoffset:0}}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="hero-002"] *{animation:none!important;transition:none!important}}
`

const ENTER =
  "animate-[vibeui-hero-002-fade-up_0.6s_cubic-bezier(0.16,1,0.3,1)_both]"

const ACTION_BASE =
  "group inline-flex h-11 items-center justify-center gap-2 rounded-lg px-5 text-[0.9375rem] font-medium focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--vibeui-hero-002-ring)]"

const SURFACE =
  "border border-[var(--vibeui-hero-002-border)] bg-[var(--vibeui-hero-002-panel)]"

// Заливка плейсхолдеров внутри mockup. Радиус задаётся на месте: у rail это
// скруглённые квадраты, у полос и точек — капсулы.
const FILL = "bg-[var(--vibeui-hero-002-border)]"

// Декоративные аватары: без имён и лиц, только градиентные заливки.
const AVATARS = [
  "linear-gradient(140deg,color-mix(in oklab,var(--vibeui-hero-002-accent) 55%,white),var(--vibeui-hero-002-accent))",
  "linear-gradient(140deg,oklch(0.76 0.1 205),oklch(0.56 0.12 215))",
  "linear-gradient(140deg,oklch(0.8 0.09 80),oklch(0.63 0.12 55))",
  "linear-gradient(140deg,oklch(0.77 0.09 330),oklch(0.57 0.14 315))",
]

const ACTIVITY_ROWS = [
  { width: "72%", tail: "18%" },
  { width: "58%", tail: "24%" },
  { width: "66%", tail: "14%" },
]

const CHART_LINE =
  "M0 92 L32 84 L64 96 L96 70 L128 78 L160 54 L192 62 L224 38 L256 46 L288 24 L320 16"

function cx(...classes: (string | false | undefined)[]) {
  return classes.filter(Boolean).join(" ")
}

function MetricTile({ label, value, delta }: Hero002Metric) {
  return (
    <div className="rounded-xl border border-[var(--vibeui-hero-002-border)] bg-[var(--vibeui-hero-002-panel-alt)] p-4">
      <p className="text-[0.75rem] font-medium tracking-wide text-[var(--vibeui-hero-002-muted)] uppercase">
        {label}
      </p>
      <div className="mt-2 flex items-baseline gap-2">
        <span className="text-[1.75rem] leading-none font-semibold tabular-nums">
          {value}
        </span>
        {delta ? (
          <span className="rounded-md bg-[color-mix(in_oklab,var(--vibeui-hero-002-positive)_14%,transparent)] px-1.5 py-0.5 text-[0.75rem] font-medium text-[var(--vibeui-hero-002-positive)]">
            {delta}
          </span>
        ) : null}
      </div>
    </div>
  )
}

// preserveAspectRatio="none" растягивает путь на всю ширину панели, а
// vector-effect держит толщину линии постоянной, поэтому растяжение не искажает
// штрих. Путь фиксированный: это иллюстрация, а не данные.
function Sparkline() {
  return (
    <svg
      viewBox="0 0 320 120"
      preserveAspectRatio="none"
      className="h-40 w-full"
      role="presentation"
    >
      <defs>
        <linearGradient id="vibeui-hero-002-fill" x1="0" y1="0" x2="0" y2="1">
          <stop
            offset="0%"
            stopColor="var(--vibeui-hero-002-accent)"
            stopOpacity="0.22"
          />
          <stop
            offset="100%"
            stopColor="var(--vibeui-hero-002-accent)"
            stopOpacity="0"
          />
        </linearGradient>
      </defs>
      <line
        x1="0"
        y1="108"
        x2="320"
        y2="108"
        stroke="var(--vibeui-hero-002-border)"
        strokeDasharray="3 5"
        vectorEffect="non-scaling-stroke"
      />
      <path
        d={`${CHART_LINE} L320 120 L0 120 Z`}
        fill="url(#vibeui-hero-002-fill)"
      />
      <path
        d={CHART_LINE}
        fill="none"
        stroke="var(--vibeui-hero-002-accent)"
        strokeWidth="2"
        strokeLinecap="round"
        strokeLinejoin="round"
        vectorEffect="non-scaling-stroke"
        style={{ strokeDasharray: 420, strokeDashoffset: 0 }}
        className="animate-[vibeui-hero-002-draw_0.9s_ease-out_0.52s_both]"
      />
    </svg>
  )
}

// Визуализация продукта — иллюстрация, а не интерфейс: вызывается внутри
// aria-hidden обёртки и не содержит ни ссылок, ни фокусируемых элементов.
function ProductWindow({
  panelTitle,
  metrics,
}: {
  panelTitle?: string
  metrics: Hero002Metric[]
}) {
  return (
    <div
      data-part="window"
      className={cx("overflow-hidden rounded-2xl", SURFACE)}
    >
      <div className="flex items-center gap-3 border-b border-[var(--vibeui-hero-002-border)] px-4 py-3">
        <span className="flex gap-1.5">
          {[0, 1, 2].map((dot) => (
            <span key={dot} className={cx("size-2 rounded-full", FILL)} />
          ))}
        </span>
        <span className="text-[0.8125rem] font-medium">{panelTitle}</span>
        <span className={cx("ml-auto h-2.5 w-10 rounded-full", FILL)} />
      </div>

      <div className="flex">
        <div
          data-part="rail"
          className="hidden shrink-0 flex-col gap-3 border-r border-[var(--vibeui-hero-002-border)] bg-[var(--vibeui-hero-002-panel-alt)] p-4"
        >
          {[0, 1, 2, 3].map((item) => (
            <span
              key={item}
              className={cx(
                "size-5 rounded-md",
                item === 0
                  ? "bg-[color-mix(in_oklab,var(--vibeui-hero-002-accent)_35%,transparent)]"
                  : FILL,
              )}
            />
          ))}
        </div>

        <div className="min-w-0 flex-1 p-4">
          <div data-part="metrics" className="grid grid-cols-1 gap-3">
            {metrics.slice(0, 2).map((metric) => (
              <MetricTile key={metric.label} {...metric} />
            ))}
          </div>

          <div className="mt-4">
            <Sparkline />
          </div>

          <div data-part="activity" className="mt-4 hidden gap-3">
            {ACTIVITY_ROWS.map((row) => (
              <div key={row.width} className="flex items-center gap-3">
                <span className={cx("size-6 shrink-0 rounded-full", FILL)} />
                <span
                  className={cx("h-2 rounded-full", FILL)}
                  style={{ width: row.width }}
                />
                <span
                  className={cx("h-2 rounded-full", FILL)}
                  style={{ width: row.tail }}
                />
              </div>
            ))}
          </div>
        </div>
      </div>
    </div>
  )
}

export function Hero002({
  eyebrow = "New · Analytics 2.0",
  title = "Every metric your team",
  titleAccent = "actually acts on",
  description = "One workspace for product analytics, alerts and reporting — without a data team in the loop.",
  primaryAction = { label: "Start free trial", href: "#" },
  secondaryAction = { label: "Book a demo", href: "#" },
  proof = "Trusted by 2,000+ product teams",
  panelTitle = "Overview",
  metrics = [
    { label: "Active users", value: "24,918", delta: "+12.4%" },
    { label: "Retention", value: "68.2%", delta: "+3.1%" },
  ],
  accent,
  accentForeground,
  className,
}: Hero002Props) {
  const style = {
    ...(accent ? { "--vibeui-hero-002-accent": accent } : {}),
    ...(accentForeground
      ? { "--vibeui-hero-002-accent-fg": accentForeground }
      : {}),
  } as CSSProperties

  return (
    <section
      data-vibeui-block="hero-002"
      style={style}
      className={cx(
        "relative isolate overflow-hidden bg-[var(--vibeui-hero-002-bg)] font-[family-name:var(--vibeui-hero-002-font)] text-[var(--vibeui-hero-002-fg)] antialiased",
        className,
      )}
    >
      <style href="vibeui-hero-002" precedence="medium">
        {STYLES}
      </style>

      <div aria-hidden="true" data-part="glow" />
      <div aria-hidden="true" data-part="dots" />

      {/* Отступы и высота живут на внутреннем слое: container-запросы читают
          ширину секции, а сама секция своим контейнером быть не может. */}
      <div
        data-part="frame"
        className="relative flex min-h-[560px] items-center px-6 py-16"
      >
        <div data-part="grid" className="grid w-full grid-cols-1 gap-10">
          <div>
            {eyebrow ? (
              <p
                className={cx(
                  ENTER,
                  SURFACE,
                  "inline-flex items-center gap-2 rounded-full px-3 py-1 text-[0.8125rem] font-medium text-[var(--vibeui-hero-002-muted)]",
                )}
              >
                <span
                  aria-hidden="true"
                  className="size-1.5 rounded-full bg-[var(--vibeui-hero-002-accent)]"
                />
                {eyebrow}
              </p>
            ) : null}

            <h1
              className={cx(
                ENTER,
                "mt-6 text-[clamp(2rem,4.1cqi,3.25rem)] leading-[1.06] font-semibold tracking-tight text-balance break-words [animation-delay:60ms]",
              )}
            >
              {title}
              {titleAccent ? (
                <>
                  {" "}
                  <span className="text-[var(--vibeui-hero-002-accent)]">
                    {titleAccent}
                  </span>
                </>
              ) : null}
            </h1>

            {description ? (
              <p
                className={cx(
                  ENTER,
                  "mt-5 max-w-[34rem] text-[clamp(1rem,1.35cqi,1.0625rem)] leading-relaxed text-pretty break-words text-[var(--vibeui-hero-002-muted)] [animation-delay:120ms]",
                )}
              >
                {description}
              </p>
            ) : null}

            <div
              data-part="actions"
              className={cx(
                ENTER,
                "mt-8 flex w-full max-w-full flex-col gap-3 [animation-delay:180ms]",
              )}
            >
              {primaryAction ? (
                <a
                  href={primaryAction.href}
                  data-part="cta-primary"
                  className={cx(
                    ACTION_BASE,
                    "bg-[var(--vibeui-hero-002-accent)] text-[var(--vibeui-hero-002-accent-fg)] transition-[filter,transform] duration-150 hover:-translate-y-px hover:brightness-110",
                  )}
                >
                  {primaryAction.label}
                </a>
              ) : null}

              {secondaryAction ? (
                <a
                  href={secondaryAction.href}
                  className={cx(
                    ACTION_BASE,
                    SURFACE,
                    "transition-colors duration-150 hover:bg-[var(--vibeui-hero-002-panel-alt)]",
                  )}
                >
                  {secondaryAction.label}
                  <svg
                    aria-hidden="true"
                    viewBox="0 0 16 16"
                    fill="none"
                    className="size-4 transition-transform duration-150 group-hover:translate-x-0.5"
                  >
                    <path
                      d="M3 8h9m0 0L8.5 4.5M12 8l-3.5 3.5"
                      stroke="currentColor"
                      strokeWidth="1.5"
                      strokeLinecap="round"
                      strokeLinejoin="round"
                    />
                  </svg>
                </a>
              ) : null}
            </div>

            {proof ? (
              <div
                className={cx(
                  ENTER,
                  "mt-8 flex items-center gap-3 [animation-delay:240ms]",
                )}
              >
                <span aria-hidden="true" className="flex">
                  {AVATARS.map((background, index) => (
                    <span
                      key={background}
                      className={cx(
                        "size-7 rounded-full border-2 border-[var(--vibeui-hero-002-bg)]",
                        index > 0 && "-ml-2",
                      )}
                      style={{ background }}
                    />
                  ))}
                </span>
                <span className="text-[0.8125rem] text-[var(--vibeui-hero-002-muted)]">
                  {proof}
                </span>
              </div>
            ) : null}
          </div>

          <div
            aria-hidden="true"
            className="relative animate-[vibeui-hero-002-fade-in_0.7s_cubic-bezier(0.16,1,0.3,1)_both] [animation-delay:260ms]"
          >
            <ProductWindow panelTitle={panelTitle} metrics={metrics} />

            <div
              data-part="chip"
              className={cx(
                "absolute -bottom-6 left-5 flex items-center gap-2 rounded-xl px-3 py-2",
                SURFACE,
              )}
            >
              <svg
                viewBox="0 0 16 16"
                fill="none"
                className="size-4 text-[var(--vibeui-hero-002-positive)]"
              >
                <path
                  d="M3.5 8.5 6.5 11.5 12.5 5"
                  stroke="currentColor"
                  strokeWidth="1.75"
                  strokeLinecap="round"
                  strokeLinejoin="round"
                />
              </svg>
              <span className="text-[0.8125rem] font-semibold">+18.2% MRR</span>
            </div>
          </div>
        </div>
      </div>
    </section>
  )
}