Features

Spotlight Features

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

  • dark
  • features
  • saas
  • editorial
  • landing
  • section

Превью

1440pxHost theme

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

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

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

# Установка и размещение «features-001» (Spotlight Features) из VibeUI

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

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

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

## 2. Что это
Тёмная секция возможностей с иерархией: полноширинная шапка, крупная панель с одной главной возможностью и абстрактной геометрией, рядом три второстепенные строки на волосяных линиях. Самодостаточна: не зависит от темы и стиля проекта.

Тёмная графитовая секция возможностей. Полноширинная шапка: акцентная черта с eyebrow, h2 в две части, описание и опциональная ссылка. Ниже асимметричное тело: слева панель с одной главной возможностью — заголовок, абзац и абстрактная геометрическая иллюстрация на inline SVG; справа ровно три второстепенные возможности строками, разделёнными волосяными линиями, с акцентными индексами. Плоско, без теней и свечения. Разделители прочерчиваются, контент проявляется со стагером. Ноль зависимостей, один файл, раскладка и типографика считаются от ширины блока через container-запросы и cqi.

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

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

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

## 5. Сохранить как установлено
- иерархию: одна главная возможность крупной панелью и ровно три второстепенные строками — не выравнивай их в сетку одинаковых карточек и не превращай в «три фичи с иконками»
- асимметричное тело от 64rem: панель шире списка; шапка остаётся полноширинной над телом
- абстрактную геометрию в панели — это иллюстрация, а не интерфейс; не превращай её в окно приложения, дашборд, график с осями или скриншот и не заменяй на <img>
- второстепенные возможности как список на волосяных линиях: без карточек, без рамок вокруг каждой, без иллюстративных иконок
- тёмную графитовую палитру --vibeui-features-001-* с низкой цветностью — не заменять на токены темы проекта (bg-background, text-foreground, primary, border) и не добавлять свечение, градиентные пятна или крупные тени
- плоскость: теней нет, тугой радиус только у панели, всё остальное держится на волосяных линиях
- h2 как заголовок секции — блок ставится под hero и не должен приносить второй h1 на страницу; заголовки возможностей остаются h3
- container-type: inline-size на корне блока и @container-правила в его <style> — блок меряет собственную ширину, а не ширину окна; не переписывай их на viewport-медиазапросы
- типографику в единицах cqi, clamp и tracking
- анимацию разделителей и одноразовый стагер; в этом блоке нет бесконечных анимаций
- блок <style> внутри компонента — в нём палитра, container-правила, keyframes и prefers-reduced-motion
- семантику: список как <ul>/<li>, диаграмма и декоративные линии aria-hidden, индексы декоративны

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

## 7. Правила
- Не добавляй четвёртую второстепенную возможность и не убирай главную: количество зон фиксировано, это часть композиции.
- Не добавляй иконки из icon-библиотек: блок собран без зависимостей.
- Не добавляй motion-библиотеки: анимации уже на CSS.
- Не выноси CSS-переменные в globals.css: блок должен оставаться одним файлом.
- Описания второстепенных возможностей держи в одну строку смысла — блок рассчитан на короткий текст, длинные абзацы ломают ритм списка.
- Если меняешь accent, проверь контраст акцентного текста на тёмном фоне: нужно не менее 4.5:1.
- Сохраняй семантику ссылок. Для внутренней навигации можно использовать компонент роутера проекта, если сохраняются визуальные стили, доступность и поведение href.

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

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

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

Блок самодостаточен: один файл, без зависимостей, всегда тёмный независимо от темы проекта. Цвета живут в локальных переменных --vibeui-features-001-*; акцент меняется пропом accent. Раскладка переключается container-запросами от ширины самого блока. Это секция, а не герой: заголовок — h2, поэтому блок можно ставить под hero, не создавая второй h1 на странице.

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

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

import type { CSSProperties } from "react"

type Features001Action = {
  label: string
  href: string
}

type Features001Feature = {
  title: string
  description: string
}

export type Features001Props = {
  eyebrow?: string
  title?: string
  titleAccent?: string
  description?: string
  action?: Features001Action
  primaryFeature?: Features001Feature
  features?: Features001Feature[]
  accent?: string
  accentForeground?: string
  className?: string
}

// Весь CSS блока живёт здесь, а не в globals.css проекта: палитра, раскладка
// и keyframes. Переменные объявлены в :where() с нулевой специфичностью,
// поэтому пользователь переопределяет их чем угодно.
//
// Тон: плоский графит с низкой цветностью. Ни свечения, ни градиентных пятен,
// ни теней — глубина строится контрастом панели и фона плюс волосяные линии.
//
// container-type делает блок собственным query-контейнером: раскладка и размер
// шрифта считаются от ширины блока, а не от ширины окна. Поэтому асимметричное
// тело видно и в миниатюре каталога, где блок рендерится в 1280px внутри
// узкого окна. Правила раскладки — плоским CSS, а не Tailwind-вариантами, чтобы
// блок не зависел от версии Tailwind в чужом проекте; специфичность (0,2,0)
// выше утилит.
const STYLES = `
:where([data-vibeui-block="features-001"]){
--vibeui-features-001-bg:oklch(0.19 0.006 260);
--vibeui-features-001-panel:oklch(0.23 0.008 260);
--vibeui-features-001-fg:oklch(0.97 0.002 260);
--vibeui-features-001-muted:oklch(0.72 0.008 260);
--vibeui-features-001-border:oklch(1 0 0 / 12%);
--vibeui-features-001-accent:oklch(0.78 0.14 175);
--vibeui-features-001-accent-fg:oklch(0.2 0.03 175);
--vibeui-features-001-accent-soft:color-mix(in oklab, var(--vibeui-features-001-accent) 16%, transparent);
--vibeui-features-001-ring:color-mix(in oklab, var(--vibeui-features-001-accent) 70%, transparent);
--vibeui-features-001-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="features-001"]{container-type:inline-size}
@container (min-width:40rem){
[data-vibeui-block="features-001"] [data-part="frame"]{min-height:620px;padding-left:2.5rem;padding-right:2.5rem}
[data-vibeui-block="features-001"] [data-part="header"]{grid-template-columns:minmax(0,1fr) auto;align-items:end}
[data-vibeui-block="features-001"] [data-part="action"]{justify-self:end}
}
@container (min-width:64rem){
[data-vibeui-block="features-001"] [data-part="frame"]{min-height:680px;padding-top:4rem;padding-bottom:4rem}
[data-vibeui-block="features-001"] [data-part="body"]{grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);gap:3rem}
}
@container (min-width:80rem){
[data-vibeui-block="features-001"] [data-part="frame"]{min-height:720px;padding-left:4rem;padding-right:4rem}
[data-vibeui-block="features-001"] [data-part="inner"]{max-width:1200px;margin-inline:auto}
[data-vibeui-block="features-001"] [data-part="body"]{gap:4rem}
}
@keyframes vibeui-features-001-fade-up{from{opacity:0;transform:translate3d(0,10px,0)}to{opacity:1;transform:none}}
@keyframes vibeui-features-001-rule{from{transform:scaleX(0)}to{transform:scaleX(1)}}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="features-001"] *{animation:none!important;transition:none!important}}
`

const ENTER =
  "animate-[vibeui-features-001-fade-up_0.5s_cubic-bezier(0.16,1,0.3,1)_both]"

const RULE =
  "origin-left animate-[vibeui-features-001-rule_0.5s_cubic-bezier(0.16,1,0.3,1)_both]"

// Стагер второстепенных возможностей: индекс строки задаёт задержку.
const ROW_DELAYS = [
  "[animation-delay:160ms]",
  "[animation-delay:220ms]",
  "[animation-delay:280ms]",
]

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

// Абстрактная геометрия: слои и связь между ними. Это иллюстрация, а не
// интерфейс — здесь намеренно нет ни шапки окна, ни кнопок, ни осей графика.
function LayerDiagram() {
  return (
    <svg
      viewBox="0 0 460 160"
      preserveAspectRatio="xMinYMid meet"
      className="mt-8 h-[clamp(6.5rem,12cqi,9rem)] w-full"
      role="presentation"
    >
      <g
        stroke="var(--vibeui-features-001-border)"
        fill="var(--vibeui-features-001-accent-soft)"
      >
        <rect x="180" y="8" width="250" height="40" rx="8" opacity="0.45" />
        <rect x="120" y="60" width="250" height="40" rx="8" opacity="0.7" />
        <rect x="60" y="112" width="250" height="40" rx="8" />
      </g>
      <path
        d="M164 28 H100 V132"
        fill="none"
        stroke="var(--vibeui-features-001-accent)"
        strokeWidth="1.5"
        strokeLinecap="round"
        strokeLinejoin="round"
        vectorEffect="non-scaling-stroke"
      />
      <circle
        cx="164"
        cy="28"
        r="3.5"
        fill="var(--vibeui-features-001-accent)"
      />
      <circle
        cx="100"
        cy="132"
        r="3.5"
        fill="var(--vibeui-features-001-accent)"
      />
    </svg>
  )
}

export function Features001({
  eyebrow = "Capabilities",
  title = "Everything after the first",
  titleAccent = "deploy",
  description = "The unglamorous half of shipping — observability, rollbacks, access control — handled before you need it.",
  action = { label: "See the full list", href: "#" },
  primaryFeature = {
    title: "Layered environments",
    description:
      "Preview, staging and production share one config and drift apart on purpose, not by accident.",
  },
  features = [
    {
      title: "Instant rollback",
      description: "Any deploy reverts to the previous build in one step.",
    },
    {
      title: "Scoped access",
      description: "Per-environment roles, no shared production credentials.",
    },
    {
      title: "Audit trail",
      description: "Every change carries an author, a reason and a timestamp.",
    },
  ],
  accent,
  accentForeground,
  className,
}: Features001Props) {
  const style = {
    ...(accent ? { "--vibeui-features-001-accent": accent } : {}),
    ...(accentForeground
      ? { "--vibeui-features-001-accent-fg": accentForeground }
      : {}),
  } as CSSProperties

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

      {/* Отступы и высота живут на внутреннем слое: container-запросы читают
          ширину секции, а сама секция своим контейнером быть не может. */}
      <div
        data-part="frame"
        className="flex min-h-[560px] items-center px-6 py-12"
      >
        <div data-part="inner" className="w-full">
          <header data-part="header" className="grid gap-6">
            <div className={ENTER}>
              {eyebrow ? (
                <p className="flex items-center gap-3 text-[0.75rem] font-medium tracking-[0.09em] text-[var(--vibeui-features-001-accent)] uppercase">
                  <span
                    aria-hidden="true"
                    className={cx(
                      RULE,
                      "h-px w-8 bg-[var(--vibeui-features-001-accent)]",
                    )}
                  />
                  {eyebrow}
                </p>
              ) : null}

              <h2 className="mt-5 max-w-[22ch] text-[clamp(1.875rem,3.4cqi,2.75rem)] leading-[1.1] font-semibold tracking-tight text-balance break-words">
                {title}
                {titleAccent ? (
                  <>
                    {" "}
                    <span className="text-[var(--vibeui-features-001-accent)]">
                      {titleAccent}
                    </span>
                  </>
                ) : null}
              </h2>

              {description ? (
                <p className="mt-4 max-w-[38rem] text-[clamp(1rem,1.25cqi,1.0625rem)] leading-relaxed text-pretty break-words text-[var(--vibeui-features-001-muted)]">
                  {description}
                </p>
              ) : null}
            </div>

            {action ? (
              <a
                data-part="action"
                href={action.href}
                className={cx(
                  ENTER,
                  "group inline-flex items-center gap-2 self-start text-[0.875rem] font-medium text-[var(--vibeui-features-001-accent)] transition-opacity duration-150 [animation-delay:60ms] hover:opacity-80 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[var(--vibeui-features-001-ring)]",
                )}
              >
                {action.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}
          </header>

          <div
            aria-hidden="true"
            className={cx(
              RULE,
              "mt-8 h-px w-full bg-[var(--vibeui-features-001-border)] [animation-delay:90ms]",
            )}
          />

          <div data-part="body" className="mt-8 grid gap-8">
            {primaryFeature ? (
              <article
                data-part="primary"
                className={cx(
                  ENTER,
                  "rounded-[10px] border border-[var(--vibeui-features-001-border)] bg-[var(--vibeui-features-001-panel)] p-6 [animation-delay:90ms]",
                )}
              >
                <h3 className="text-[1.25rem] font-medium">
                  {primaryFeature.title}
                </h3>
                <p className="mt-3 max-w-[34rem] text-[0.9375rem] leading-relaxed text-pretty text-[var(--vibeui-features-001-muted)]">
                  {primaryFeature.description}
                </p>
                <LayerDiagram />
              </article>
            ) : null}

            {features.length > 0 ? (
              <ul
                data-part="secondary"
                className="flex flex-col justify-center"
              >
                {features.slice(0, 3).map((feature, index) => (
                  <li
                    key={feature.title}
                    className={cx(
                      ENTER,
                      ROW_DELAYS[index],
                      "flex gap-4 py-5",
                      index > 0 &&
                        "border-t border-[var(--vibeui-features-001-border)]",
                    )}
                  >
                    <span
                      aria-hidden="true"
                      className="pt-1 text-[0.75rem] text-[var(--vibeui-features-001-accent)] tabular-nums"
                    >
                      0{index + 1}
                    </span>
                    <div className="min-w-0">
                      <h3 className="text-[1rem] font-medium">
                        {feature.title}
                      </h3>
                      <p className="mt-1.5 text-[0.875rem] leading-relaxed text-pretty break-words text-[var(--vibeui-features-001-muted)]">
                        {feature.description}
                      </p>
                    </div>
                  </li>
                ))}
              </ul>
            ) : null}
          </div>
        </div>
      </div>
    </section>
  )
}