Hero
Console Hero
Светлый B2B SaaS hero со split-раскладкой: слева текст и пара CTA, справа декоративная визуализация продукта. Самодостаточен: не зависит от темы и стиля проекта.
- light
- saas
- split
- dashboard
- b2b
- landing
Превью
Использовать с AI
- 1. Скопируйте ссылку.
- 2. Напишите агенту своими словами и вставьте её в предложение.
- 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>
)
}