Navigation

Thumb Pagination

Пагинация для телефона: три крупные цели вместо ряда номеров, а полный список страниц открывается сеткой в popover.

  • pagination
  • mobile
  • touch
  • popover

Превью

1440pxHost theme

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

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

размести это в шапке: https://vibeui.ru/c/pagination-009?page=18

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

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

# Установка и размещение «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.json
https://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>
    </>
  )
}