Navigation

Cursor Pagination

Курсорная навигация: диапазон строк вместо номеров страниц — он остаётся верным, когда данные меняются на ходу.

  • pagination
  • cursor
  • list
  • range

Превью

1440pxHost theme

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

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

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

Показать полную инструкцию

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

# Установка и размещение «pagination-002» (Cursor Pagination) из VibeUI

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

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

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

## 2. Что это
Курсорная навигация: диапазон строк вместо номеров страниц — он остаётся верным, когда данные меняются на ходу.

Курсорная навигация: диапазон строк слева, кнопки «Назад» и «Вперёд» справа, край списка приглушён. Ноль зависимостей, один файл, клиентского JS нет.

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

<Pagination002 from={41} perPage={20} total={214} />

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

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

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

## 5. Сохранить как установлено
- локальную палитру --vibeui-pagination-002-* и не подменять её токенами вашей темы
- счёт по строкам, а не по страницам: при меняющихся данных номер страницы врёт
- aria-disabled на краю списка вместо удаления кнопки — иначе строка прыгает
- табличные цифры в диапазоне
- ссылки, а не кнопки: адрес страницы должен открываться в новой вкладке
- стрелки из бордюров: они поворачиваются вместе с направлением

## 6. Можно менять
- from, perPage и total
- prevHref и nextHref
- акцент через проп accent
- ширину строки

## 7. Правила
- Компонент только показывает границы: курсор и запрос держит вызывающий код.
- Номеров страниц нет намеренно: для стабильных данных с переходом на страницу берите pagination-001.
- pointer-events:none на краю не отменяет переход по клавиатуре — уберите href, если нужен полный запрет.
- Не выносите CSS-переменные в globals.css: компонент обязан оставаться одним файлом.

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

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

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

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-pagination-002-*. Серверный: хуков нет, границы считаются из пропсов. Край списка помечен aria-disabled на ссылке: кнопка остаётся на месте, но не работает, поэтому строка навигации не прыгает. Числа набраны табличными цифрами.

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

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

import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Pagination002Props = Omit<
  ComponentPropsWithoutRef<"nav">,
  "children"
> & {
  /** Номер первой строки на экране: счёт ведут по строкам, а не по страницам. */
  from?: number
  perPage?: number
  total?: number
  prevHref?: string
  nextHref?: string
  accent?: string
}

// Идея компонента: курсорная навигация для длинных списков. Номеров страниц
// нет намеренно: при постоянно меняющихся данных страница №7 завтра покажет
// другие строки, а «строки 41–60 из 214» остаётся правдой. Край списка
// закрывает недоступную кнопку через aria-disabled на ссылке: убирать её
// нельзя, иначе строка навигации прыгает.
const STYLES = `
:where([data-vibeui-block="pagination-002"]){
--vibeui-pagination-002-bg:oklch(1 0 0);
--vibeui-pagination-002-fg:oklch(0.24 0.014 265);
--vibeui-pagination-002-muted:oklch(0.56 0.014 265);
--vibeui-pagination-002-border:oklch(0.9 0.006 265);
--vibeui-pagination-002-hover:oklch(0.55 0.02 265 / 8%);
--vibeui-pagination-002-accent:oklch(0.55 0.2 262);
--vibeui-pagination-002-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="pagination-002"]{
display:flex;align-items:center;justify-content:space-between;gap:1rem;
width:100%;max-width:26rem;box-sizing:border-box;padding:0.5rem 0.625rem;
background:var(--vibeui-pagination-002-bg);
border:1px solid var(--vibeui-pagination-002-border);border-radius:0.75rem;
font-family:var(--vibeui-pagination-002-font);color:var(--vibeui-pagination-002-fg);
}
/* Счёт по строкам: он остаётся верным, даже когда данные меняются. */
[data-vibeui-block="pagination-002"] [data-part="range"]{
font-size:0.8125rem;color:var(--vibeui-pagination-002-muted);
font-variant-numeric:tabular-nums;
}
[data-vibeui-block="pagination-002"] [data-part="range"] b{color:var(--vibeui-pagination-002-fg);font-weight:650}
[data-vibeui-block="pagination-002"] [data-part="side"]{display:flex;align-items:center;gap:0.375rem}
[data-vibeui-block="pagination-002"] [data-part="step"]{
display:inline-flex;align-items:center;gap:0.375rem;
height:2rem;padding:0 0.625rem;border-radius:0.5rem;
border:1px solid var(--vibeui-pagination-002-border);
color:inherit;text-decoration:none;font-size:0.8125rem;
}
[data-vibeui-block="pagination-002"] [data-part="step"]:hover{background:var(--vibeui-pagination-002-hover)}
[data-vibeui-block="pagination-002"] [data-part="step"]:focus-visible{outline:2px solid var(--vibeui-pagination-002-accent);outline-offset:2px}
/* Край списка: кнопка остаётся на месте, но не работает — строка не прыгает. */
[data-vibeui-block="pagination-002"] [data-part="step"][aria-disabled="true"]{
color:var(--vibeui-pagination-002-muted);pointer-events:none;opacity:.55;
}
[data-vibeui-block="pagination-002"] [data-part="arrow"]{
width:0.375rem;height:0.375rem;
border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;
}
[data-vibeui-block="pagination-002"] [data-part="step"][data-dir="prev"] [data-part="arrow"]{transform:rotate(135deg)}
[data-vibeui-block="pagination-002"] [data-part="step"][data-dir="next"] [data-part="arrow"]{transform:rotate(-45deg)}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="pagination-002"] *{animation:none!important;transition:none!important}}
`

/**
 * Курсорная навигация: диапазон строк вместо номеров страниц.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Pagination002({
  from = 41,
  perPage = 20,
  total = 214,
  prevHref = "#",
  nextHref = "#",
  accent,
  className,
  style,
  ...props
}: Pagination002Props) {
  const to = Math.min(from + perPage - 1, total)
  const atStart = from <= 1
  const atEnd = to >= total

  const palette = {
    ...(accent ? { "--vibeui-pagination-002-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-pagination-002" precedence="medium">
        {STYLES}
      </style>
      <nav
        {...props}
        data-vibeui-block="pagination-002"
        aria-label="Навигация по списку"
        className={className}
        style={palette}
      >
        <p data-part="range">
          Строки{" "}
          <b>
            {from}–{to}
          </b>{" "}
          из {total}
        </p>
        <div data-part="side">
          <a
            data-part="step"
            data-dir="prev"
            href={prevHref}
            aria-disabled={atStart || undefined}
          >
            <span data-part="arrow" aria-hidden="true" />
            Назад
          </a>
          <a
            data-part="step"
            data-dir="next"
            href={nextHref}
            aria-disabled={atEnd || undefined}
          >
            Вперёд
            <span data-part="arrow" aria-hidden="true" />
          </a>
        </div>
      </nav>
    </>
  )
}