Navigation

Jumping Ellipsis

Пагинация со свёрткой середины, где «…» не подпись, а кнопка: она перепрыгивает через пропущенный кусок сразу на пять страниц.

  • pagination
  • ellipsis
  • jump
  • list

Превью

1440pxHost theme

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

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

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

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

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

# Установка и размещение «pagination-003» (Jumping Ellipsis) из VibeUI

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

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

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

## 2. Что это
Пагинация со свёрткой середины, где «…» не подпись, а кнопка: она перепрыгивает через пропущенный кусок сразу на пять страниц.

Пагинация с окном номеров вокруг текущей страницы и свёрткой середины. Многоточие — живая кнопка: наведение или фокус превращают его в стрелку, а нажатие переносит сразу на пять страниц. Ноль зависимостей, один файл, своя палитра.

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

<Pagination003 total={24} defaultPage={7} siblings={1} />

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

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

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

## 5. Сохранить как установлено
- локальную палитру --vibeui-pagination-003-* и не подменять её токенами вашей темы (bg-primary, border-input и подобными)
- многоточие кнопкой, а не подписью: обычный разрыв — тупик, из него до середины списка идут по одной странице
- aria-label у кнопки прыжка: «…» скринридер прочитает как символ и не объяснит, куда она ведёт
- aria-current="page" у текущей страницы: без него активный номер отличается только цветом
- заливку активного номера вместе с насыщенностью — один цвет теряется рядом с наведением
- табличные цифры у номеров: без них ширина кнопок пляшет от цифр

## 6. Можно менять
- total и defaultPage — количество страниц и та, что открыта изначально
- siblings — сколько номеров показывать вокруг текущей
- величину прыжка многоточия (константа JUMP)
- акцент через проп accent — цвет активного номера и колец фокуса

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

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

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

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

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-pagination-003-*. Клиентский: "use client" ради состояния текущей страницы. Окно номеров считает функция windowOf: первая, последняя, соседи и разрывы между ними; разрыв помечен нулём, и на его месте рисуется кнопка прыжка. Многоточие превращается в стрелку по :hover и :focus-visible. Текущая страница помечена aria-current="page", числа набраны табличными цифрами.

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

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

"use client"

import { useState } from "react"
import type { CSSProperties } from "react"

export type Pagination003Props = {
  total?: number
  defaultPage?: number
  siblings?: number
  accent?: string
  className?: string
  style?: CSSProperties
}

// Идея компонента: пагинация, где «…» — не подпись, а кнопка. Свёрнутая середина
// обычно тупик: до тридцатой страницы приходится идти по одной. Здесь многоточие
// перепрыгивает через пропущенный кусок, поэтому длинный список проходится за
// несколько нажатий, а ряд при этом остаётся одной ширины.
const STYLES = `
:where([data-vibeui-block="pagination-003"]){
--vibeui-pagination-003-bg:oklch(1 0 0);
--vibeui-pagination-003-fg:oklch(0.24 0.014 265);
--vibeui-pagination-003-muted:oklch(0.55 0.014 265);
--vibeui-pagination-003-border:oklch(0.91 0.006 265);
--vibeui-pagination-003-hover:oklch(0.55 0.02 265 / 8%);
--vibeui-pagination-003-accent:oklch(0.55 0.2 262);
--vibeui-pagination-003-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="pagination-003"]{
box-sizing:border-box;width:100%;max-width:32rem;padding:0.5rem;
display:flex;align-items:center;justify-content:center;gap:0.25rem;flex-wrap:wrap;
background:var(--vibeui-pagination-003-bg);color:var(--vibeui-pagination-003-fg);
border:1px solid var(--vibeui-pagination-003-border);border-radius:0.75rem;
font-family:var(--vibeui-pagination-003-font);
}
[data-vibeui-block="pagination-003"] [data-part="cell"]{
appearance:none;cursor:pointer;
min-width:2rem;height:2rem;padding:0 0.375rem;box-sizing:border-box;
display:inline-flex;align-items:center;justify-content:center;
border:1px solid transparent;border-radius:0.5rem;background:none;
font:inherit;font-size:0.8125rem;color:inherit;
font-variant-numeric:tabular-nums;
transition:background-color .14s ease,border-color .14s ease;
}
[data-vibeui-block="pagination-003"] [data-part="cell"]:hover:not(:disabled){background:var(--vibeui-pagination-003-hover)}
[data-vibeui-block="pagination-003"] [data-part="cell"]:focus-visible{outline:2px solid var(--vibeui-pagination-003-accent);outline-offset:1px}
[data-vibeui-block="pagination-003"] [data-part="cell"]:disabled{color:var(--vibeui-pagination-003-muted);cursor:default;opacity:.55}
/* Текущая страница: заливка плюс жирность — цвет один не отличает её от наведения. */
[data-vibeui-block="pagination-003"] [data-part="cell"][aria-current="page"]{
background:var(--vibeui-pagination-003-accent);color:oklch(1 0 0);font-weight:650;
}
/* Многоточие превращается в стрелку прыжка, когда до него дотянулись. */
[data-vibeui-block="pagination-003"] [data-skip="true"]{color:var(--vibeui-pagination-003-muted)}
[data-vibeui-block="pagination-003"] [data-skip="true"] [data-part="jump"]{display:none}
[data-vibeui-block="pagination-003"] [data-skip="true"]:hover [data-part="dots"],
[data-vibeui-block="pagination-003"] [data-skip="true"]:focus-visible [data-part="dots"]{display:none}
[data-vibeui-block="pagination-003"] [data-skip="true"]:hover [data-part="jump"],
[data-vibeui-block="pagination-003"] [data-skip="true"]:focus-visible [data-part="jump"]{display:inline;color:var(--vibeui-pagination-003-accent)}
[data-vibeui-block="pagination-003"] [data-edge="true"]{padding:0 0.625rem;color:var(--vibeui-pagination-003-muted)}
[data-vibeui-block="pagination-003"] [data-edge="true"]:hover:not(:disabled){color:var(--vibeui-pagination-003-fg)}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="pagination-003"] *{animation:none!important;transition:none!important}}
`

const JUMP = 5

/**
 * Окно номеров вокруг текущей страницы: первая, последняя, соседи и разрывы.
 * Разрыв отмечается нулём — на его месте рисуется кнопка прыжка.
 */
function windowOf(page: number, total: number, siblings: number) {
  const pages = new Set<number>([1, total])

  for (let step = -siblings; step <= siblings; step += 1) {
    const value = page + step

    if (value >= 1 && value <= total) {
      pages.add(value)
    }
  }

  const sorted = [...pages].sort((left, right) => left - right)
  const result: number[] = []

  sorted.forEach((value, index) => {
    if (index > 0 && value - sorted[index - 1] > 1) {
      result.push(0)
    }

    result.push(value)
  })

  return result
}

/**
 * Пагинация со свёрткой середины, где «…» перепрыгивает через пропущенное.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Pagination003({
  total = 24,
  defaultPage = 7,
  siblings = 1,
  accent,
  className,
  style,
}: Pagination003Props) {
  const [page, setPage] = useState(Math.min(Math.max(defaultPage, 1), total))

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

  function go(value: number) {
    setPage(Math.min(Math.max(value, 1), total))
  }

  const cells = windowOf(page, total, siblings)

  return (
    <>
      <style href="vibeui-pagination-003" precedence="medium">
        {STYLES}
      </style>
      <nav
        data-vibeui-block="pagination-003"
        aria-label="Страницы результатов"
        className={className}
        style={palette}
      >
        <button
          type="button"
          data-part="cell"
          data-edge="true"
          disabled={page === 1}
          aria-label="Предыдущая страница"
          onClick={() => go(page - 1)}
        >
          ← Назад
        </button>
        {cells.map((value, index) =>
          value === 0 ? (
            <button
              key={`skip-${index}`}
              type="button"
              data-part="cell"
              data-skip="true"
              aria-label={
                index < cells.indexOf(page)
                  ? `Назад на ${JUMP} страниц`
                  : `Вперёд на ${JUMP} страниц`
              }
              onClick={() =>
                go(index < cells.indexOf(page) ? page - JUMP : page + JUMP)
              }
            >
              <span data-part="dots">…</span>
              <span data-part="jump">
                {index < cells.indexOf(page) ? "«" : "»"}
              </span>
            </button>
          ) : (
            <button
              key={value}
              type="button"
              data-part="cell"
              aria-current={value === page ? "page" : undefined}
              aria-label={`Страница ${value}`}
              onClick={() => go(value)}
            >
              {value}
            </button>
          ),
        )}
        <button
          type="button"
          data-part="cell"
          data-edge="true"
          disabled={page === total}
          aria-label="Следующая страница"
          onClick={() => go(page + 1)}
        >
          Вперёд →
        </button>
      </nav>
    </>
  )
}