Navigation
Thumb Pagination
Пагинация для телефона: три крупные цели вместо ряда номеров, а полный список страниц открывается сеткой в popover.
- pagination
- mobile
- touch
- popover
Превью
Использовать с AI
- 1. Скопируйте ссылку.
- 2. Напишите агенту своими словами и вставьте её в предложение.
- 3. Агент откроет ссылку и поставит компонент из registry.
размести это в шапке: https://vibeui.ru/c/pagination-009?page=15
Показать полную инструкцию
То, что лежит по ссылке в развёрнутом виде. Нужна, если агент не может открыть ссылку — тогда вставьте этот текст целиком.
# Установка и размещение «pagination-009» (Thumb Pagination) из VibeUI
## 1. Сначала установка — не пропускай, не пересоздавай
Выполни эту команду до того, как писать код:
npx shadcn@latest add https://vibeui.ru/r/pagination-009.json
Registry item: https://vibeui.ru/r/pagination-009.json
Ставится в: components/vibeui/pagination-009.tsx (точный путь следует алиасам components.json этого проекта).
npm-зависимости: нет.
Registry-зависимости: нет.
Ставь из реестра. Не пересоздавай по описанию, не подменяй похожим
компонентом из другой библиотеки и не переписывай под стиль проекта.
## 2. Что это
Пагинация для телефона: три крупные цели вместо ряда номеров, а полный список страниц открывается сеткой в popover.
Компактная пагинация для телефона: крупные стрелки по краям и кнопка с текущей страницей посередине, открывающая сетку всех номеров. Все переходы — ссылки, поэтому страница открывается в новой вкладке и остаётся в истории. Ноль зависимостей, один файл, клиентского JS нет.
## 3. Как использовать
import { Pagination009 } from "@/components/vibeui/pagination-009"
<Pagination009 page={3} total={18} hrefOf={(page) => `/catalog?page=${page}`} />
Полный список пропсов — в установленном файле.
## 4. Куда поставить
Это мелкий inline-компонент. Поставь его ровно туда, куда просил
пользователь, внутрь существующей разметки. Не создавай ради него
новую страницу, секцию или обёртку. Если на этом месте уже стоит похожий
контрол — замени его, а не добавляй второй.
Куда поставить: ___
(Эту строку заполняет пользователь. Если она осталась пустой — спроси,
а не угадывай.)
## 5. Сохранить как установлено
- локальную палитру --vibeui-pagination-009-* и не подменять её токенами вашей темы (bg-card, border-input и подобными)
- размер целей не меньше 2.75rem: ряд из мелких номеров на телефоне не берётся пальцем
- сетку номеров в popover вместо ряда: восемнадцать номеров в строку на узком экране не помещаются
- popover="auto": Escape и касание мимо закрывают сетку без вашего кода
- aria-current="page" у текущего номера и в списке, и на кнопке-указателе
- aria-label у стрелок: стрелка из бордюров не имеет текста и без подписи безымянна
## 6. Можно менять
- page и total — текущая страница и общее количество
- hrefOf — как собрать адрес страницы под ваш роутер
- акцент через проп accent — цвет активного номера и колец фокуса
- размер целей и число колонок в сетке
## 7. Правила
- Сетка выводит все страницы: на нескольких сотнях она станет длинным списком с прокруткой.
- Без поддержки anchor positioning сетка открывается по центру экрана: на телефоне это приемлемо, но проверьте.
- Компонент рассчитан на узкую ширину: на десктопе берите ряд номеров.
- Не выносите CSS-переменные в globals.css: компонент обязан оставаться одним файлом.
## 8. Проверка
- рендерится без ошибок в консоли;
- выглядит так же, как превью на странице VibeUI, откуда взята ссылка;
- если нет — ты изменил что-то из раздела 5, верни как было.Для разработчика
npx shadcn@latest add https://vibeui.ru/r/pagination-009.jsonhttps://vibeui.ru/r/pagination-009.jsonКомпонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-pagination-009-*. Серверный: хуков нет, все номера — обычные ссылки. Цели по 2.75rem — нижняя граница для касания пальцем. Сетка страниц лежит в popover="auto" с якорем на самой панели, поэтому закрытие по Escape и по касанию мимо достаётся от браузера. Текущая страница помечена aria-current="page", края гасятся через aria-disabled.
Исходник компонента
Тот же файл, который поставит агент. Нужен, если вы предпочитаете скопировать код руками.
import type { CSSProperties } from "react"
export type Pagination009Props = {
page?: number
total?: number
hrefOf?: (page: number) => string
accent?: string
className?: string
style?: CSSProperties
}
// Идея компонента: пагинация для телефона. Ряд номеров на узком экране нечитаем
// и в него не попасть пальцем, поэтому здесь три крупные цели: назад, вперёд и
// текущая страница, открывающая сетку всех номеров. Сетка — HTML popover: она
// закрывается по Escape и по касанию мимо без единой строки JS.
const STYLES = `
:where([data-vibeui-block="pagination-009"]){
--vibeui-pagination-009-bg:oklch(1 0 0);
--vibeui-pagination-009-fg:oklch(0.24 0.014 265);
--vibeui-pagination-009-muted:oklch(0.55 0.014 265);
--vibeui-pagination-009-border:oklch(0.91 0.006 265);
--vibeui-pagination-009-hover:oklch(0.55 0.02 265 / 8%);
--vibeui-pagination-009-accent:oklch(0.55 0.2 262);
--vibeui-pagination-009-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="pagination-009"]{
box-sizing:border-box;width:100%;max-width:20rem;padding:0.375rem;
display:flex;align-items:center;gap:0.375rem;
background:var(--vibeui-pagination-009-bg);color:var(--vibeui-pagination-009-fg);
border:1px solid var(--vibeui-pagination-009-border);border-radius:0.875rem;
font-family:var(--vibeui-pagination-009-font);
anchor-name:--vibeui-pagination-009-bar;
}
/* Цели по 2.75rem: меньше пальцем не попасть, это нижняя граница для касания. */
[data-vibeui-block="pagination-009"] [data-part="step"]{
width:2.75rem;height:2.75rem;flex:none;
display:inline-grid;place-items:center;
border:1px solid var(--vibeui-pagination-009-border);border-radius:0.75rem;
text-decoration:none;color:inherit;
}
[data-vibeui-block="pagination-009"] [data-part="step"]:hover{background:var(--vibeui-pagination-009-hover)}
[data-vibeui-block="pagination-009"] [data-part="step"]:focus-visible{outline:2px solid var(--vibeui-pagination-009-accent);outline-offset:2px}
[data-vibeui-block="pagination-009"] [data-part="step"][aria-disabled="true"]{opacity:.4;pointer-events:none}
[data-vibeui-block="pagination-009"] [data-part="arrow"]{
width:0.5rem;height:0.5rem;
border:1.75px solid currentColor;border-right:0;border-bottom:0;transform:rotate(-45deg);
margin-left:0.1875rem;
}
[data-vibeui-block="pagination-009"] [data-part="step"][data-dir="next"] [data-part="arrow"]{transform:rotate(135deg);margin:0 0.1875rem 0 0}
[data-vibeui-block="pagination-009"] [data-part="current"]{
flex:1;height:2.75rem;appearance:none;cursor:pointer;
display:inline-flex;align-items:center;justify-content:center;gap:0.375rem;
border:1px solid var(--vibeui-pagination-009-border);border-radius:0.75rem;
background:none;color:inherit;font:inherit;font-size:0.9375rem;
font-variant-numeric:tabular-nums;
}
[data-vibeui-block="pagination-009"] [data-part="current"]:focus-visible{outline:2px solid var(--vibeui-pagination-009-accent);outline-offset:2px}
[data-vibeui-block="pagination-009"] [data-part="of"]{color:var(--vibeui-pagination-009-muted);font-size:0.8125rem}
[data-vibeui-block="pagination-009"] [data-part="sheet"]{
position:fixed;inset:auto;margin:0;
width:min(18rem,92vw);max-height:14rem;overflow-y:auto;
padding:0.5rem;box-sizing:border-box;
background:var(--vibeui-pagination-009-bg);color:var(--vibeui-pagination-009-fg);
border:1px solid var(--vibeui-pagination-009-border);border-radius:0.875rem;
font-family:var(--vibeui-pagination-009-font);
box-shadow:0 24px 48px -24px oklch(0.2 0.03 265 / 40%);
}
@supports (anchor-name: --a){
[data-vibeui-block="pagination-009"] [data-part="sheet"]{
position-anchor:--vibeui-pagination-009-bar;
position-area:top span-right;margin-bottom:0.5rem;
position-try-fallbacks:flip-block;
}
}
[data-vibeui-block="pagination-009"] [data-part="grid"]{
margin:0;padding:0;list-style:none;
display:grid;grid-template-columns:repeat(auto-fill,minmax(2.5rem,1fr));gap:0.25rem;
}
[data-vibeui-block="pagination-009"] [data-part="num"]{
height:2.5rem;display:grid;place-items:center;border-radius:0.5rem;
text-decoration:none;color:inherit;font-size:0.875rem;font-variant-numeric:tabular-nums;
}
[data-vibeui-block="pagination-009"] [data-part="num"]:hover{background:var(--vibeui-pagination-009-hover)}
[data-vibeui-block="pagination-009"] [data-part="num"]:focus-visible{outline:2px solid var(--vibeui-pagination-009-accent);outline-offset:-2px}
[data-vibeui-block="pagination-009"] [data-part="num"][aria-current="page"]{
background:var(--vibeui-pagination-009-accent);color:oklch(1 0 0);font-weight:650;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="pagination-009"] *{animation:none!important;transition:none!important}}
`
/**
* Компактная пагинация для телефона: крупные стрелки и сетка номеров.
* Один файл, ноль зависимостей, собственная палитра, клиентского JS нет.
*/
export function Pagination009({
page = 3,
total = 18,
hrefOf = (value: number) => `?page=${value}`,
accent,
className,
style,
}: Pagination009Props) {
const palette = {
...(accent ? { "--vibeui-pagination-009-accent": accent } : null),
...style,
} as CSSProperties
const first = page <= 1
const last = page >= total
return (
<>
<style href="vibeui-pagination-009" precedence="medium">
{STYLES}
</style>
<nav
data-vibeui-block="pagination-009"
aria-label="Навигация по страницам"
className={className}
style={palette}
>
<a
data-part="step"
data-dir="prev"
href={first ? undefined : hrefOf(page - 1)}
aria-disabled={first || undefined}
aria-label="Предыдущая страница"
rel="prev"
>
<span data-part="arrow" aria-hidden="true" />
</a>
<button
type="button"
data-part="current"
aria-haspopup="true"
popoverTarget="vibeui-pagination-009-sheet"
>
{page}
<span data-part="of">из {total}</span>
</button>
<a
data-part="step"
data-dir="next"
href={last ? undefined : hrefOf(page + 1)}
aria-disabled={last || undefined}
aria-label="Следующая страница"
rel="next"
>
<span data-part="arrow" aria-hidden="true" />
</a>
<div
id="vibeui-pagination-009-sheet"
data-part="sheet"
popover="auto"
aria-label="Все страницы"
>
<ul data-part="grid">
{Array.from({ length: total }, (unused, index) => index + 1).map(
(value) => (
<li key={value}>
<a
data-part="num"
href={hrefOf(value)}
aria-current={value === page ? "page" : undefined}
>
{value}
</a>
</li>
),
)}
</ul>
</div>
</nav>
</>
)
}