Feedback

Quota Banner

A plan-limit bar with a number instead of a scare: the fill is computed from two figures and the colour switches itself once less than a tenth is left.

  • banner
  • quota
  • plan
  • upgrade

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/banner-005?lang=en

Тариф «Старт»9 200 из 10 000 запросов

Лимит обновится первого числа. До этого момента остаток не переносится.

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 "banner-005" (Quota Banner) 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/banner-005.json

Registry item: https://vibeui.ru/r/banner-005.json
Installs to: components/vibeui/banner-005.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 plan-limit bar with a number instead of a scare: the fill is computed from two figures and the colour switches itself once less than a tenth is left.

A plan-limit bar: a ratio from two numbers, a self-switching tone and an upgrade button. Zero dependencies, one file, no client JS.

## 3. How to use it
import { Banner005 } from "@/components/vibeui/banner-005"

<Banner005
  used={9200}
  limit={10000}
  unit="requests"
  actionLabel="Upgrade plan"
/>

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-banner-005-* palette — do not swap it for your theme tokens
- the figures next to the bar: "almost out" without numbers cannot support a decision
- switching the tone by ratio rather than writing a rule set per state
- role="progressbar" with aria-valuenow — the ratio has to be announced
- tabular figures on the counter: otherwise the numbers jump on every update
- the card's own light surface: without it the dark text disappears on a dark background

## 6. You may change
- the figures through used and limit, the unit through unit
- the plan name through planLabel
- the button caption through actionLabel
- the calm tone through the --vibeui-banner-005-tone variable

## 7. Rules
- The ratio is capped at a hundred: overuse shows as a full bar plus separate copy.
- The thresholds live in the code: 90 percent and exhaustion — change them together with the wording.
- Numbers are formatted with the Russian locale: for another locale pass pre-formatted strings.
- 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/banner-005.json
https://vibeui.ru/r/banner-005.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-banner-005-*. Серверный: доля считается из used и limit, кладётся в --vibeui-banner-005-ratio, а ширину полосы выводит calc. Состояние (calm, tight, over) объявлено атрибутом data-state и переопределяет один тон — отдельных наборов правил под каждое состояние нет. Полоса размечена role="progressbar" с aria-valuenow. Раскладка считается от собственной ширины через @container на внутреннем shell.

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 Banner005Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children"
> & {
  /** Израсходовано и включено в тариф: полоса считается из этих двух чисел. */
  used?: number
  limit?: number
  unit?: string
  planLabel?: string
  actionLabel?: string
}

// Идея компонента: полоса тарифного лимита, которая показывает цифру, а не
// пугает словами. Заполнение считается из двух чисел, и цвет переключается
// сам: до 90 процентов — спокойный, дальше — предупреждающий.
const STYLES = `
:where([data-vibeui-block="banner-005"]){
--vibeui-banner-005-bg:oklch(1 0 0);
--vibeui-banner-005-fg:oklch(0.24 0.014 265);
--vibeui-banner-005-muted:oklch(0.54 0.014 265);
--vibeui-banner-005-border:oklch(0.9 0.006 265);
--vibeui-banner-005-track:oklch(0.93 0.006 265);
--vibeui-banner-005-tone:oklch(0.6 0.15 250);
--vibeui-banner-005-ratio:0;
--vibeui-banner-005-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
container-type:inline-size;
}
[data-vibeui-block="banner-005"]{
width:100%;box-sizing:border-box;
font-family:var(--vibeui-banner-005-font);color:var(--vibeui-banner-005-fg);
}
[data-vibeui-block="banner-005"][data-state="tight"]{--vibeui-banner-005-tone:oklch(0.62 0.18 45)}
[data-vibeui-block="banner-005"][data-state="over"]{--vibeui-banner-005-tone:oklch(0.58 0.2 25)}
[data-vibeui-block="banner-005"] [data-part="shell"]{
display:grid;grid-template-columns:1fr auto;gap:0.5rem 1rem;align-items:center;
box-sizing:border-box;padding:0.9375rem 1.125rem;
border:1px solid var(--vibeui-banner-005-border);border-radius:1rem;
background:var(--vibeui-banner-005-bg);
}
[data-vibeui-block="banner-005"] [data-part="head"]{display:flex;align-items:baseline;gap:0.5rem;flex-wrap:wrap;min-width:0}
[data-vibeui-block="banner-005"] [data-part="plan"]{
flex:none;padding:0.0625rem 0.4375rem;border-radius:0.375rem;
background:color-mix(in oklab,var(--vibeui-banner-005-tone) 16%,transparent);
color:var(--vibeui-banner-005-tone);
font-size:0.6875rem;font-weight:700;letter-spacing:0.03em;text-transform:uppercase;
}
[data-vibeui-block="banner-005"] [data-part="counter"]{font-size:0.875rem;font-weight:640;font-variant-numeric:tabular-nums}
[data-vibeui-block="banner-005"] [data-part="counter"] span{color:var(--vibeui-banner-005-muted);font-weight:500}
/* Полоса считается из двух чисел: разметка не знает про проценты. */
[data-vibeui-block="banner-005"] [data-part="track"]{
grid-column:1 / -1;position:relative;overflow:hidden;
height:0.375rem;border-radius:9999px;background:var(--vibeui-banner-005-track);
}
[data-vibeui-block="banner-005"] [data-part="fill"]{
position:absolute;inset:0 auto 0 0;border-radius:inherit;
width:calc(var(--vibeui-banner-005-ratio) * 1%);
background:var(--vibeui-banner-005-tone);
transition:width .3s ease,background-color .3s ease;
}
[data-vibeui-block="banner-005"] [data-part="note"]{
grid-column:1 / -1;margin:0;font-size:0.8125rem;line-height:1.45;color:var(--vibeui-banner-005-muted);
}
[data-vibeui-block="banner-005"] [data-part="action"]{
appearance:none;cursor:pointer;border:0;
height:2.125rem;padding:0 0.9375rem;border-radius:0.625rem;
background:var(--vibeui-banner-005-tone);color:oklch(0.99 0.01 265);
font:inherit;font-size:0.8125rem;font-weight:650;white-space:nowrap;
transition:filter .16s ease;
}
[data-vibeui-block="banner-005"] [data-part="action"]:hover{filter:brightness(1.08)}
[data-vibeui-block="banner-005"] [data-part="action"]:focus-visible{outline:2px solid var(--vibeui-banner-005-tone);outline-offset:2px}
@container (max-width: 26rem){
[data-vibeui-block="banner-005"] [data-part="shell"]{grid-template-columns:1fr}
[data-vibeui-block="banner-005"] [data-part="action"]{width:100%}
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="banner-005"] *{animation:none!important;transition:none!important}}
`

/**
 * Полоса тарифного лимита: заполнение из двух чисел и кнопка апгрейда.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Banner005({
  used = 9200,
  limit = 10000,
  unit = "запросов",
  planLabel = "Тариф «Старт»",
  actionLabel = "Повысить тариф",
  className,
  style,
  ...props
}: Banner005Props) {
  const safeLimit = limit > 0 ? limit : 1
  const ratio = Math.min(100, Math.round((used / safeLimit) * 100))
  const state = used >= safeLimit ? "over" : ratio >= 90 ? "tight" : "calm"
  const palette = {
    "--vibeui-banner-005-ratio": ratio,
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-banner-005" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="banner-005"
        data-state={state}
        role="status"
        aria-live="polite"
        className={className}
        style={palette}
      >
        <div data-part="shell">
          <div data-part="head">
            <span data-part="plan">{planLabel}</span>
            <span data-part="counter">
              {used.toLocaleString("ru-RU")}{" "}
              <span>
                из {limit.toLocaleString("ru-RU")} {unit}
              </span>
            </span>
          </div>
          <button data-part="action" type="button">
            {actionLabel}
          </button>
          <div
            data-part="track"
            role="progressbar"
            aria-valuenow={ratio}
            aria-valuemin={0}
            aria-valuemax={100}
            aria-label={`Израсходовано ${ratio} процентов лимита`}
          >
            <span data-part="fill" />
          </div>
          <p data-part="note">
            {state === "over"
              ? "Лимит исчерпан: новые запросы отклоняются до начала следующего периода."
              : "Лимит обновится первого числа. До этого момента остаток не переносится."}
          </p>
        </div>
      </div>
    </>
  )
}