Inputs

Batch Progress

One bar for the whole batch: what matters is how long is left, while the per-file detail hides inside a disclosure list.

  • file
  • upload
  • progress
  • batch

Preview

1440pxHost theme

Use it with AI

  1. 1. Copy the link.
  2. 2. Write to your agent in your own words and drop the link into the sentence.
  3. 3. The agent opens the link and installs the component from the registry.

put this in the header: https://vibeui.ru/c/file-007?lang=en

Gallery upload

60%

Готово 3 из 5. Вкладку можно не держать открытой.

Подробности по файлам
  • 01-обложка.jpgготов
  • 02-разворот.jpgготов
  • 03-детали.jpgготов
  • 04-упаковка.jpgидёт
  • 05-макро.jpgв очереди
Show the full instructions

What the link says, spelled out. Use it when your agent cannot open links — paste this text instead.

# Install and place "file-007" (Batch Progress) from VibeUI

## 1. Install first — do not skip, do not recreate
Run this exact command before writing any code:
npx shadcn@latest add https://vibeui.ru/r/file-007.json

Registry item: https://vibeui.ru/r/file-007.json
Installs to: components/vibeui/file-007.tsx (the exact path follows this project's components.json aliases).
npm dependencies: none.
Registry dependencies: none.

Install it from the registry. Do not recreate it from the description,
do not substitute a similar component from another library, and do not
rewrite it to match the project's existing style.

## 2. What it is
One bar for the whole batch: what matters is how long is left, while the per-file detail hides inside a disclosure list.

A batch upload summary: one bar for the whole set, a percentage, a done-N-of-M counter and per-file detail in a disclosure. The disclosure is a native <details>, so the component stays server-side. Zero dependencies, one file, its own palette.

## 3. How to use it
import { File007 } from "@/components/vibeui/file-007"

<File007 title="Gallery upload" done={3} total={5} />

Read the installed file for the full prop list.

## 4. Where to place it
This is a small inline component. Put it exactly where the user asked,
inside the existing markup. Do not create a new page, section or
wrapper for it. If a similar control already sits in that spot,
replace it instead of adding a second one.

Placement: ___
(The user fills this line in. If it is still blank, ask where to put it
instead of guessing.)

## 5. Keep exactly as installed
- the local --vibeui-file-007-* palette — do not swap it for your theme tokens (bg-primary, bg-muted and the like)
- one bar for the whole batch: twenty separate progress bars do not read, only the waiting time matters
- the native <progress> with max set to the file count and a meaningful aria-label — share and role come for free
- the detail inside <details>: the list is needed occasionally but takes room always
- the percentage computed from the same done and total as the counter: two different numbers side by side read as a bug
- the "done N of M" line beside the percentage — a share with no counts gives no sense of what is left
- the <style> block inside the component — it holds the palette, the bar and the disclosure arrow

## 6. You may change
- the title above the bar
- done and total — how many files are finished and how many there are
- the entries array of file names and states
- the accent through the accent prop — it colours the bar
- the line under the bar

## 7. Rules
- The component uploads nothing: the numbers arrive as props and the caller updates them.
- Readiness is counted in files, not bytes: five small files and one huge one give a misleading estimate.
- Cancelling individual files needs a list with its own buttons — here the list only reports state.
- Do not lift the CSS variables into globals.css: the component has to stay a single file.

## 8. Verify
- it renders with no console errors;
- it looks like the preview on the VibeUI page you copied this from;
- if it does not, you changed something listed in section 5 — put it back.

For developers

npx shadcn@latest add https://vibeui.ru/r/file-007.json
https://vibeui.ru/r/file-007.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-file-007-*. Серверный: раскрытие держит нативный <details>, состояния нет и гидрации тоже. Общая доля показана нативным <progress> с max по числу файлов и aria-label вида «Загружено N из M», процент посчитан из тех же чисел. Список файлов лежит внутри <details> и не занимает места, пока не нужен.

Component source

The same file your agent installs. Here in case you would rather copy it by hand.

import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type File007Entry = {
  name: string
  state: "готов" | "идёт" | "в очереди"
}

export type File007Props = Omit<ComponentPropsWithoutRef<"div">, "children"> & {
  title?: string
  done?: number
  total?: number
  entries?: File007Entry[]
  accent?: string
}

// Идея компонента: одна полоска на всю пачку. Двадцать отдельных прогрессов
// не читаются — важен ответ на вопрос «сколько ещё ждать», а не судьба
// каждого файла. Общая доля стоит наверху нативным <progress>, а подробности
// спрятаны в <details>: список открывается без единой строчки JS, поэтому
// компонент остаётся серверным и ничего не гидрирует.
const STYLES = `
:where([data-vibeui-block="file-007"]){
--vibeui-file-007-surface:oklch(1 0 0);
--vibeui-file-007-fg:oklch(0.23 0.014 265);
--vibeui-file-007-muted:oklch(0.55 0.014 265);
--vibeui-file-007-border:oklch(0.89 0.008 265);
--vibeui-file-007-shell:oklch(0.91 0.006 265);
--vibeui-file-007-track:oklch(0.93 0.006 265);
--vibeui-file-007-accent:oklch(0.54 0.17 268);
--vibeui-file-007-ok:oklch(0.52 0.13 155);
--vibeui-file-007-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
/* Своя светлая подложка: сводку показывают поверх любого фона. */
[data-vibeui-block="file-007"]{
display:flex;flex-direction:column;gap:0.5rem;
width:100%;max-width:23rem;box-sizing:border-box;padding:0.875rem;
background:var(--vibeui-file-007-surface);
border:1px solid var(--vibeui-file-007-shell);border-radius:0.875rem;
font-family:var(--vibeui-file-007-font);color:var(--vibeui-file-007-fg);
}
[data-vibeui-block="file-007"] *{box-sizing:border-box}
[data-vibeui-block="file-007"] [data-part="head"]{
display:flex;align-items:baseline;justify-content:space-between;gap:0.75rem;
}
[data-vibeui-block="file-007"] h3{margin:0;font-size:0.8125rem;font-weight:650}
[data-vibeui-block="file-007"] [data-part="share"]{
font-size:0.8125rem;font-weight:700;font-variant-numeric:tabular-nums;
color:var(--vibeui-file-007-accent);
}
/* Одна полоска на всю пачку: важно «сколько ещё ждать», а не судьба каждого файла. */
[data-vibeui-block="file-007"] progress{
appearance:none;width:100%;height:0.4375rem;border:0;
background:var(--vibeui-file-007-track);border-radius:9999px;overflow:hidden;
}
[data-vibeui-block="file-007"] progress::-webkit-progress-bar{background:var(--vibeui-file-007-track);border-radius:9999px}
[data-vibeui-block="file-007"] progress::-webkit-progress-value{background:var(--vibeui-file-007-accent);border-radius:9999px}
[data-vibeui-block="file-007"] progress::-moz-progress-bar{background:var(--vibeui-file-007-accent);border-radius:9999px}
[data-vibeui-block="file-007"] [data-part="count"]{
margin:0;font-size:0.75rem;color:var(--vibeui-file-007-muted);
font-variant-numeric:tabular-nums;
}
/* Подробности в <details>: раскрытие без единой строчки JS. */
[data-vibeui-block="file-007"] details{
border-top:1px solid var(--vibeui-file-007-border);padding-top:0.5rem;
}
[data-vibeui-block="file-007"] summary{
cursor:pointer;list-style:none;display:flex;align-items:center;gap:0.375rem;
font-size:0.75rem;font-weight:650;color:var(--vibeui-file-007-muted);
}
[data-vibeui-block="file-007"] summary::-webkit-details-marker{display:none}
[data-vibeui-block="file-007"] summary::before{
content:"";width:0.375rem;height:0.375rem;
border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;
transform:rotate(-45deg);transition:transform .16s ease;
}
[data-vibeui-block="file-007"] details[open] summary::before{transform:rotate(45deg)}
[data-vibeui-block="file-007"] summary:focus-visible{outline:2px solid var(--vibeui-file-007-accent);outline-offset:2px;border-radius:0.25rem}
[data-vibeui-block="file-007"] ul{
display:flex;flex-direction:column;gap:0.1875rem;
margin:0.5rem 0 0;padding:0;list-style:none;
}
[data-vibeui-block="file-007"] li{
display:flex;align-items:center;justify-content:space-between;gap:0.5rem;
font-size:0.75rem;
}
[data-vibeui-block="file-007"] li span:first-child{
min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
[data-vibeui-block="file-007"] [data-part="state"]{flex:none;color:var(--vibeui-file-007-muted);font-size:0.6875rem}
[data-vibeui-block="file-007"] li[data-state="готов"] [data-part="state"]{color:var(--vibeui-file-007-ok);font-weight:650}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="file-007"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_ENTRIES: File007Entry[] = [
  { name: "01-обложка.jpg", state: "готов" },
  { name: "02-разворот.jpg", state: "готов" },
  { name: "03-детали.jpg", state: "готов" },
  { name: "04-упаковка.jpg", state: "идёт" },
  { name: "05-макро.jpg", state: "в очереди" },
]

/**
 * Множественная загрузка одной общей полоской и списком в <details>.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function File007({
  title = "Загрузка галереи",
  done = 3,
  total = 5,
  entries = DEFAULT_ENTRIES,
  accent,
  className,
  style,
  ...props
}: File007Props) {
  const share = total > 0 ? Math.round((done / total) * 100) : 0

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

  return (
    <>
      <style href="vibeui-file-007" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="file-007"
        className={className}
        style={palette}
      >
        <div data-part="head">
          <h3>{title}</h3>
          <span data-part="share">{share}%</span>
        </div>
        <progress
          max={total}
          value={done}
          aria-label={`Загружено ${done} из ${total} файлов`}
        />
        <p data-part="count">
          Готово {done} из {total}. Вкладку можно не держать открытой.
        </p>
        <details>
          <summary>Подробности по файлам</summary>
          <ul>
            {entries.map((entry) => (
              <li key={entry.name} data-state={entry.state}>
                <span>{entry.name}</span>
                <span data-part="state">{entry.state}</span>
              </li>
            ))}
          </ul>
        </details>
      </div>
    </>
  )
}