Data Grid
Infinite Scroll
Длинная сетка с догрузкой по прокрутке: счётчик и полоса показывают, сколько строк уже подгружено, а кнопка «Загрузить ещё» повторяет автозагрузку для клавиатуры.
- datagrid
- infinite-scroll
- progress
- table
Превью
Использовать с AI
- 1. Скопируйте ссылку.
- 2. Напишите агенту своими словами и вставьте её в предложение.
- 3. Агент откроет ссылку и поставит компонент из registry.
размести это в шапке: https://vibeui.ru/c/datagrid-025
Лента событий кассы
Загружено 12 из 48
| Время | Событие | Источник | Сумма, ₽ |
|---|---|---|---|
| 12:59 | Заказ оплачен | касса-1 | 120 |
| 12:58 | Возврат оформлен | касса-2 | 157 |
| 12:57 | Скидка применена | терминал | 194 |
| 12:56 | Чек аннулирован | самовывоз | 231 |
| 12:55 | Заказ собран | маркетплейс | 268 |
| 12:54 | Доставка назначена | касса-1 | 305 |
| 12:53 | Заказ оплачен | касса-2 | 342 |
| 12:52 | Возврат оформлен | терминал | 379 |
| 12:51 | Скидка применена | самовывоз | 416 |
| 12:50 | Чек аннулирован | маркетплейс | 453 |
| 12:49 | Заказ собран | касса-1 | 490 |
| 12:48 | Доставка назначена | касса-2 | 527 |
Показать полную инструкцию
То, что лежит по ссылке в развёрнутом виде. Нужна, если агент не может открыть ссылку — тогда вставьте этот текст целиком.
# Установка и размещение «datagrid-025» (Infinite Scroll) из VibeUI
## 1. Сначала установка — не пропускай, не пересоздавай
Выполни эту команду до того, как писать код:
npx shadcn@latest add https://vibeui.ru/r/datagrid-025.json
Registry item: https://vibeui.ru/r/datagrid-025.json
Ставится в: components/vibeui/datagrid-025.tsx (точный путь следует алиасам components.json этого проекта).
npm-зависимости: нет.
Registry-зависимости: нет.
Ставь из реестра. Не пересоздавай по описанию, не подменяй похожим
компонентом из другой библиотеки и не переписывай под стиль проекта.
## 2. Что это
Длинная сетка с догрузкой по прокрутке: счётчик и полоса показывают, сколько строк уже подгружено, а кнопка «Загрузить ещё» повторяет автозагрузку для клавиатуры.
Сетка с бесконечной прокруткой, счётчиком загруженного и полосой прогресса. Ноль зависимостей, один файл.
## 3. Как использовать
import { Datagrid025 } from "@/components/vibeui/datagrid-025"
<Datagrid025 caption="Лента событий" batchSize={20} />
Полный список пропсов — в установленном файле.
## 4. Куда поставить
Это мелкий inline-компонент. Поставь его ровно туда, куда просил
пользователь, внутрь существующей разметки. Не создавай ради него
новую страницу, секцию или обёртку. Если на этом месте уже стоит похожий
контрол — замени его, а не добавляй второй.
Куда поставить: ___
(Эту строку заполняет пользователь. Если она осталась пустой — спроси,
а не угадывай.)
## 5. Сохранить как установлено
- локальную палитру --vibeui-datagrid-025-* и не подменять её токенами вашей темы
- кнопку «Загрузить ещё» рядом с автозагрузкой: одна прокрутка отрезает клавиатуру
- запас в 24 px до дна — без него порог не ловится при быстрой прокрутке
- счётчик и полосу с aria-valuenow: бесконечный список без видимого дна дезориентирует
- липкую шапку с собственным фоном, иначе на глубине непонятно, что за колонки
- aria-live у счётчика загруженного, чтобы догрузка была слышна, а не только видна
## 6. Можно менять
- подпись под шапкой через caption
- размер порции догрузки через batchSize
- ленту событий через rows
- акцентный цвет через accent
## 7. Правила
- Данные лежат в памяти целиком: под настоящую подгрузку замените срез запросом за следующей страницей.
- Порог догрузки в пикселях, а не в процентах: при высоте окна больше порции список догрузится сразу дважды.
- Состояния «загружается» нет — при реальном запросе добавьте флаг, иначе повтор уйдёт дважды.
- Не выносите CSS-переменные в globals.css: компонент обязан оставаться одним файлом.
## 8. Проверка
- рендерится без ошибок в консоли;
- выглядит так же, как превью на странице VibeUI, откуда взята ссылка;
- если нет — ты изменил что-то из раздела 5, верни как было.Для разработчика
npx shadcn@latest add https://vibeui.ru/r/datagrid-025.jsonhttps://vibeui.ru/r/datagrid-025.jsonКомпонент самодостаточен: один файл, без зависимостей, палитра в --vibeui-datagrid-025-*. Клиентский: useState хранит число показанных строк, срез массива даёт видимые. Автозагрузка висит на onScroll контейнера и срабатывает за 24 px до дна — без запаса порог не ловится при быстрой прокрутке. Кнопка «Загрузить ещё» оставлена намеренно: она дублирует автозагрузку для клавиатуры и для тех, кто прыгнул к концу списка. Доля загруженного показана и числом, и полосой с role=progressbar и aria-valuenow: бесконечный список без дна дезориентирует. Шапка липнет сверху, поэтому колонки видны на любой глубине.
Исходник компонента
Тот же файл, который поставит агент. Нужен, если вы предпочитаете скопировать код руками.
"use client"
import { useState } from "react"
import type { ComponentPropsWithoutRef, CSSProperties } from "react"
export type Datagrid025Row = {
id: string
time: string
event: string
source: string
weight: number
}
export type Datagrid025Props = Omit<
ComponentPropsWithoutRef<"section">,
"children"
> & {
rows?: Datagrid025Row[]
caption?: string
batchSize?: number
accent?: string
}
// Идея компонента: длинный список догружается по мере прокрутки, но
// счётчик и кнопка остаются. Автозагрузка срабатывает у нижней кромки
// окна прокрутки, а «Загрузить ещё» дублирует её для клавиатуры и для
// тех, кто дошёл до низа рывком. Полоса прогресса показывает, сколько
// из общего числа уже подгружено — иначе бесконечный список не имеет дна.
const STYLES = `
:where([data-vibeui-block="datagrid-025"]){
--vibeui-datagrid-025-bg:oklch(1 0 0);
--vibeui-datagrid-025-fg:oklch(0.23 0.014 285);
--vibeui-datagrid-025-muted:oklch(0.55 0.014 285);
--vibeui-datagrid-025-border:oklch(0.92 0.006 285);
--vibeui-datagrid-025-head:oklch(0.975 0.003 285);
--vibeui-datagrid-025-accent:oklch(0.52 0.16 265);
--vibeui-datagrid-025-height:17rem;
--vibeui-datagrid-025-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="datagrid-025"]{
box-sizing:border-box;width:100%;max-width:48rem;margin:0 auto;
background:var(--vibeui-datagrid-025-bg);color:var(--vibeui-datagrid-025-fg);
border:1px solid var(--vibeui-datagrid-025-border);border-radius:0.875rem;
font-family:var(--vibeui-datagrid-025-font);overflow:hidden;
}
[data-vibeui-block="datagrid-025"] *{box-sizing:border-box}
[data-vibeui-block="datagrid-025"] [data-part="bar"]{
display:flex;flex-wrap:wrap;align-items:center;gap:0.5rem;
padding:0.75rem 0.875rem 0.625rem;border-bottom:1px solid var(--vibeui-datagrid-025-border);
}
[data-vibeui-block="datagrid-025"] [data-part="title"]{margin:0;font-size:0.875rem;font-weight:650;margin-inline-end:auto}
[data-vibeui-block="datagrid-025"] [data-part="count"]{margin:0;font-size:0.75rem;color:var(--vibeui-datagrid-025-muted)}
[data-vibeui-block="datagrid-025"] [data-part="track"]{
flex:0 0 100%;height:0.25rem;border-radius:999px;margin-top:0.125rem;
background:var(--vibeui-datagrid-025-border);overflow:hidden;
}
[data-vibeui-block="datagrid-025"] [data-part="fill"]{
display:block;height:100%;border-radius:999px;
width:var(--vibeui-datagrid-025-progress,0%);background:var(--vibeui-datagrid-025-accent);
transition:width .2s ease;
}
[data-vibeui-block="datagrid-025"] [data-part="scroll"]{
overflow:auto;max-height:var(--vibeui-datagrid-025-height);
}
[data-vibeui-block="datagrid-025"] [data-part="scroll"]:focus-visible{outline:2px solid var(--vibeui-datagrid-025-accent);outline-offset:-2px}
[data-vibeui-block="datagrid-025"] table{width:100%;border-collapse:separate;border-spacing:0;font-size:0.8125rem}
[data-vibeui-block="datagrid-025"] caption{
padding:0.625rem 0.875rem;text-align:left;font-size:0.75rem;color:var(--vibeui-datagrid-025-muted);caption-side:top;
}
[data-vibeui-block="datagrid-025"] th,
[data-vibeui-block="datagrid-025"] td{
padding:0.4375rem 0.875rem;text-align:left;white-space:nowrap;
border-bottom:1px solid var(--vibeui-datagrid-025-border);
}
[data-vibeui-block="datagrid-025"] thead th{
position:sticky;top:0;z-index:2;background:var(--vibeui-datagrid-025-head);font-weight:600;
}
[data-vibeui-block="datagrid-025"] [data-align="end"]{text-align:right;font-variant-numeric:tabular-nums}
[data-vibeui-block="datagrid-025"] [data-part="time"]{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:0.75rem;font-weight:500}
[data-vibeui-block="datagrid-025"] [data-part="source"]{color:var(--vibeui-datagrid-025-muted)}
[data-vibeui-block="datagrid-025"] [data-part="foot"]{
display:flex;align-items:center;justify-content:center;gap:0.5rem;
padding:0.625rem 0.875rem;border-top:1px solid var(--vibeui-datagrid-025-border);
}
[data-vibeui-block="datagrid-025"] [data-part="more"]{
appearance:none;cursor:pointer;font:inherit;font-size:0.75rem;font-weight:600;
padding:0.375rem 0.875rem;border-radius:0.5rem;
border:1px solid var(--vibeui-datagrid-025-accent);background:transparent;color:var(--vibeui-datagrid-025-accent);
}
[data-vibeui-block="datagrid-025"] [data-part="more"]:focus-visible{outline:2px solid var(--vibeui-datagrid-025-accent);outline-offset:2px}
[data-vibeui-block="datagrid-025"] [data-part="done"]{margin:0;font-size:0.75rem;color:var(--vibeui-datagrid-025-muted)}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="datagrid-025"] *{animation:none!important;transition:none!important}}
`
const SOURCES = ["касса-1", "касса-2", "терминал", "самовывоз", "маркетплейс"]
const EVENTS = [
"Заказ оплачен",
"Возврат оформлен",
"Скидка применена",
"Чек аннулирован",
"Заказ собран",
"Доставка назначена",
]
const DEFAULT_ROWS: Datagrid025Row[] = Array.from(
{ length: 48 },
(_, index) => ({
id: `l${index + 1}`,
time: `12:${String(59 - index).padStart(2, "0")}`,
event: EVENTS[index % EVENTS.length],
source: SOURCES[index % SOURCES.length],
weight: 120 + ((index * 37) % 880),
}),
)
/**
* Сетка с бесконечной прокруткой и счётчиком загруженного: порция
* догружается у нижней кромки и по кнопке. Один файл, ноль зависимостей.
*/
export function Datagrid025({
rows = DEFAULT_ROWS,
caption = "Порция догружается у нижней кромки окна прокрутки",
batchSize = 12,
accent,
className,
style,
...props
}: Datagrid025Props) {
const [shown, setShown] = useState(batchSize)
const visible = rows.slice(0, shown)
const done = shown >= rows.length
const percent = Math.round((visible.length / rows.length) * 100)
const palette = {
"--vibeui-datagrid-025-progress": `${percent}%`,
...(accent ? { "--vibeui-datagrid-025-accent": accent } : null),
...style,
} as CSSProperties
function loadMore() {
setShown((current) => Math.min(rows.length, current + batchSize))
}
return (
<>
<style href="vibeui-datagrid-025" precedence="medium">
{STYLES}
</style>
<section
{...props}
data-vibeui-block="datagrid-025"
className={className}
style={palette}
>
<div data-part="bar">
<h3 data-part="title">Лента событий кассы</h3>
<p data-part="count" role="status" aria-live="polite">
Загружено {visible.length} из {rows.length}
</p>
<span
data-part="track"
role="progressbar"
aria-label="Доля загруженных строк"
aria-valuenow={percent}
aria-valuemin={0}
aria-valuemax={100}
>
<span data-part="fill" />
</span>
</div>
<div
data-part="scroll"
role="region"
aria-label="Лента событий, прокручивается"
tabIndex={0}
onScroll={(event) => {
const box = event.currentTarget
if (
!done &&
box.scrollTop + box.clientHeight >= box.scrollHeight - 24
) {
loadMore()
}
}}
>
<table>
<caption>{caption}</caption>
<thead>
<tr>
<th scope="col">Время</th>
<th scope="col">Событие</th>
<th scope="col">Источник</th>
<th scope="col" data-align="end">
Сумма, ₽
</th>
</tr>
</thead>
<tbody>
{visible.map((row) => (
<tr key={row.id}>
<th scope="row" data-part="time">
{row.time}
</th>
<td>{row.event}</td>
<td data-part="source">{row.source}</td>
<td data-align="end">{row.weight.toLocaleString("ru-RU")}</td>
</tr>
))}
</tbody>
</table>
</div>
<div data-part="foot">
{done ? (
<p data-part="done">Все {rows.length} строк загружены</p>
) : (
<button type="button" data-part="more" onClick={loadMore}>
Загрузить ещё {Math.min(batchSize, rows.length - shown)}
</button>
)}
</div>
</section>
</>
)
}