Display

Upload Speed Row

A file upload row that answers "how much longer" rather than "how many percent": size, speed and time left are derived from three numbers and cannot contradict each other.

  • progress
  • upload
  • file
  • display

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/progress-004?lang=en

quarterly-report-2026.pdf
7.0 МБ из 12.0 МБ1.0 МБ/с · осталось 5 с
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 "progress-004" (Upload Speed Row) 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/progress-004.json

Registry item: https://vibeui.ru/r/progress-004.json
Installs to: components/vibeui/progress-004.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
A file upload row that answers "how much longer" rather than "how many percent": size, speed and time left are derived from three numbers and cannot contradict each other.

File upload: the ratio, the size, the speed and the time left all derive from loaded, total and speed, so the captions never drift apart. Zero dependencies, one file, server rendered.

## 3. How to use it
import { Progress004 } from "@/components/vibeui/progress-004"

<Progress004
  fileName="quarterly-report-2026.pdf"
  loaded={7340032}
  total={12582912}
  speed={1048576}
/>

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-progress-004-* palette — do not swap it for your theme tokens
- deriving every caption from the three numbers: a separately computed percent will eventually disagree with the size
- the aria-valuetext carrying the time left: a bare ratio does not answer "how long"
- the tabular figures in the stats row — otherwise it jitters on every update
- the ellipsized long file name with the full name in title
- the row's own light surface: without it the dark text disappears on a dark background

## 6. You may change
- the file name through fileName
- the transferred size through loaded and the full size through total
- the throughput through speed — the time left is derived from it
- the fill colour through the accent variable

## 7. Rules
- A speed of zero yields an infinite remainder: the component prints "almost done", not "Infinity".
- Sizes use binary prefixes: 1 MB here is 1048576 bytes, not a million.
- The component draws no cancel control: put the button beside it so it does not fight for row width.
- 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/progress-004.json
https://vibeui.ru/r/progress-004.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-progress-004-*. Серверный: хуков нет, форматирование байтов и остатка — чистые функции. Проценты, «7 МБ из 12 МБ» и «осталось 5 с» выводятся из loaded, total и speed, поэтому подписи согласованы по построению. Значок файла — плитка с срезанным углом на clip-path и расширением текстом, отдельной иконки не нужно. Дорожка растёт шириной от переменной значения с transition, а не пересобирает раскладку. Цифры моноширинные: при обычных числа дёргаются на каждом обновлении.

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 Progress004Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children"
> & {
  fileName?: string
  /** Передано и всего — в байтах: проценты и остаток считаются из них. */
  loaded?: number
  total?: number
  /** Байт в секунду: из неё берутся и скорость, и оставшееся время. */
  speed?: number
}

// Идея компонента: строка загрузки файла отвечает не «сколько процентов», а
// «сколько ещё ждать». Проценты, объём, скорость и остаток считаются из трёх
// чисел, поэтому подписи не могут разойтись между собой. Цифры моноширинные:
// при обычных они дёргаются на каждом обновлении.
const STYLES = `
:where([data-vibeui-block="progress-004"]){
--vibeui-progress-004-bg:oklch(1 0 0);
--vibeui-progress-004-fg:oklch(0.25 0.016 265);
--vibeui-progress-004-muted:oklch(0.56 0.014 265);
--vibeui-progress-004-border:oklch(0.9 0.006 265);
--vibeui-progress-004-track:oklch(0.93 0.005 265);
--vibeui-progress-004-accent:oklch(0.55 0.19 262);
--vibeui-progress-004-value:0;
--vibeui-progress-004-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="progress-004"]{
display:flex;gap:0.875rem;align-items:flex-start;
width:100%;max-width:26rem;box-sizing:border-box;padding:0.875rem 1rem;
background:var(--vibeui-progress-004-bg);
border:1px solid var(--vibeui-progress-004-border);border-radius:0.875rem;
font-family:var(--vibeui-progress-004-font);color:var(--vibeui-progress-004-fg);
}
[data-vibeui-block="progress-004"] [data-part="icon"]{
flex:none;display:grid;place-items:center;
width:2.25rem;height:2.75rem;border-radius:0.375rem;
background:oklch(0.96 0.012 262);color:var(--vibeui-progress-004-accent);
font-size:0.5625rem;font-weight:800;letter-spacing:0.06em;
clip-path:polygon(0 0,72% 0,100% 26%,100% 100%,0 100%);
}
[data-vibeui-block="progress-004"] [data-part="body"]{
display:flex;flex-direction:column;gap:0.375rem;flex:1 1 auto;min-width:0;
}
[data-vibeui-block="progress-004"] [data-part="name"]{
font-size:0.8125rem;font-weight:650;
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
[data-vibeui-block="progress-004"] [data-part="track"]{
height:0.375rem;border-radius:9999px;overflow:hidden;
background:var(--vibeui-progress-004-track);
}
[data-vibeui-block="progress-004"] [data-part="bar"]{
height:100%;border-radius:inherit;
width:calc(var(--vibeui-progress-004-value) * 1%);
background:var(--vibeui-progress-004-accent);
transition:width .3s cubic-bezier(.32,.72,0,1);
}
/* Три показателя в одну строку: объём слева, скорость и остаток справа. */
[data-vibeui-block="progress-004"] [data-part="stats"]{
display:flex;justify-content:space-between;gap:0.75rem;
font-size:0.6875rem;color:var(--vibeui-progress-004-muted);
font-variant-numeric:tabular-nums;
}
[data-vibeui-block="progress-004"] [data-part="stats"] span{
overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
@media (prefers-reduced-motion:reduce){
[data-vibeui-block="progress-004"] *{animation:none!important;transition:none!important}
}
`

function formatBytes(bytes: number) {
  const units = ["Б", "КБ", "МБ", "ГБ"]
  let size = Math.max(0, bytes)
  let unit = 0

  while (size >= 1024 && unit < units.length - 1) {
    size /= 1024
    unit += 1
  }

  return `${size >= 100 || unit === 0 ? Math.round(size) : size.toFixed(1)} ${units[unit]}`
}

function formatLeft(seconds: number) {
  if (!Number.isFinite(seconds) || seconds <= 0) {
    return "почти готово"
  }

  if (seconds < 60) {
    return `осталось ${Math.ceil(seconds)} с`
  }

  return `осталось ${Math.ceil(seconds / 60)} мин`
}

/**
 * Загрузка файла со скоростью и оставшимся временем.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Progress004({
  fileName = "quarterly-report-2026.pdf",
  loaded = 7_340_032,
  total = 12_582_912,
  speed = 1_048_576,
  className,
  style,
  ...props
}: Progress004Props) {
  const size = Math.max(1, total)
  const done = Math.min(size, Math.max(0, loaded))
  const percent = Math.round((done / size) * 100)
  const left = speed > 0 ? (size - done) / speed : Number.POSITIVE_INFINITY
  const extension =
    fileName.split(".").pop()?.slice(0, 4).toUpperCase() ?? "FILE"
  const palette = {
    "--vibeui-progress-004-value": percent,
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-progress-004" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="progress-004"
        className={className}
        style={palette}
      >
        <span data-part="icon" aria-hidden="true">
          {extension}
        </span>
        <div data-part="body">
          <span data-part="name" title={fileName}>
            {fileName}
          </span>
          <div
            data-part="track"
            role="progressbar"
            aria-label={`Загрузка ${fileName}`}
            aria-valuemin={0}
            aria-valuemax={100}
            aria-valuenow={percent}
            aria-valuetext={`${percent} процентов, ${formatLeft(left)}`}
          >
            <div data-part="bar" />
          </div>
          <div data-part="stats">
            <span>
              {formatBytes(done)} из {formatBytes(size)}
            </span>
            <span>
              {formatBytes(speed)}/с · {formatLeft(left)}
            </span>
          </div>
        </div>
      </div>
    </>
  )
}