Navigation
Jumping Ellipsis
Пагинация со свёрткой середины, где «…» не подпись, а кнопка: она перепрыгивает через пропущенный кусок сразу на пять страниц.
- pagination
- ellipsis
- jump
- list
Превью
Использовать с AI
- 1. Скопируйте ссылку.
- 2. Напишите агенту своими словами и вставьте её в предложение.
- 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.jsonhttps://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>
</>
)
}