Inputs

Deadline Warning

A deadline field that says out loud the date has passed: browsers treat a past date as valid and stay silent.

  • date
  • deadline
  • warning
  • task

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/date-006?lang=en

Срок сверяется с сегодняшним днём после загрузки.

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 "date-006" (Deadline Warning) 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/date-006.json

Registry item: https://vibeui.ru/r/date-006.json
Installs to: components/vibeui/date-006.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 deadline field that says out loud the date has passed: browsers treat a past date as valid and stay silent.

A deadline field with three states: overdue, due soon, time left. The comparison with today happens after mount. Zero dependencies, one file.

## 3. How to use it
import { Date006 } from "@/components/vibeui/date-006"

<Date006 label="Due date" defaultValue="2026-09-04" soonInDays={3} />

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-date-006-* palette — do not swap it for your theme tokens
- its own light surface: the field gets shown over any background
- reading today through useSyncExternalStore with an empty server snapshot: computing it during render makes server and client disagree at hydration
- the neutral state before the first client snapshot: flashing a false alarm is worse than showing it one frame later
- data-state driving a single accent variable: otherwise the colour has to be patched in five rules
- aria-live on the status line: the warning appears without a reload
- the left stripe instead of an icon: colour reads faster in peripheral vision than a glyph

## 6. You may change
- label — the field caption
- defaultValue — the starting deadline
- soonInDays — how many days ahead counts as due soon
- the wording of the three states
- the accent through the accent prop

## 7. Rules
- The comparison uses the device's local midnight: compute on the server for strict deadlines.
- The component warns but does not forbid: a past date stays selectable on purpose.
- accent overrides the state colour — set it only when the traffic light is unwanted.
- 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/date-006.json
https://vibeui.ru/r/date-006.json

Компонент самодостаточен: один файл, ноль зависимостей, палитра в локальных переменных --vibeui-date-006-*. Клиентский: дата в состоянии, а сегодняшний день читается через useSyncExternalStore — серверный снимок пустой, поэтому сравнивать нечего и гидратация не расходится; считать «сегодня» при отрисовке значит получить на сервере и на клиенте разные даты. Состояний три: прошло, скоро, в запасе; они помечаются data-state на корне и переопределяют одну переменную акцента, поэтому цвет рамки, полосы и текста меняется разом. До первого клиентского снимка состояние нейтральное.

Component source

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

"use client"

import { useId, useState, useSyncExternalStore } from "react"
import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Date006Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children" | "defaultValue"
> & {
  label?: string
  defaultValue?: string
  soonInDays?: number
  accent?: string
}

// Идея компонента: срок, который сам говорит, что он в прошлом. Прошедшая дата
// формально валидна, поэтому браузер о ней молчит, а задача с таким сроком
// тихо уезжает в просрочку. Компонент сравнивает значение с сегодняшним днём и
// показывает одно из трёх состояний: прошло, скоро, в запасе. Сегодняшний день
// вычисляется после монтирования: считать его при отрисовке значит получить на
// сервере и на клиенте разные даты и расхождение при гидратации. До первого
// эффекта состояние нейтральное — это честнее, чем мигнуть ложной тревогой.
const STYLES = `
:where([data-vibeui-block="date-006"]){
--vibeui-date-006-surface:oklch(1 0 0);
--vibeui-date-006-field:oklch(1 0 0);
--vibeui-date-006-shell:oklch(0.9 0.006 265);
--vibeui-date-006-fg:oklch(0.23 0.014 265);
--vibeui-date-006-muted:oklch(0.55 0.014 265);
--vibeui-date-006-border:oklch(0.88 0.008 265);
--vibeui-date-006-ok:oklch(0.55 0.14 160);
--vibeui-date-006-soon:oklch(0.66 0.15 70);
--vibeui-date-006-past:oklch(0.56 0.19 25);
--vibeui-date-006-accent:var(--vibeui-date-006-ok);
--vibeui-date-006-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
/* Своя светлая подложка: поле показывают поверх любого фона. */
[data-vibeui-block="date-006"]{
display:flex;flex-direction:column;gap:0.5rem;
width:100%;max-width:20rem;box-sizing:border-box;padding:0.875rem;
background:var(--vibeui-date-006-surface);
border:1px solid var(--vibeui-date-006-shell);border-radius:0.875rem;
font-family:var(--vibeui-date-006-font);color:var(--vibeui-date-006-fg);
}
/* Одна переменная на состояние: цвет рамки, полосы и текста меняется разом. */
[data-vibeui-block="date-006"][data-state="soon"]{--vibeui-date-006-accent:var(--vibeui-date-006-soon)}
[data-vibeui-block="date-006"][data-state="past"]{--vibeui-date-006-accent:var(--vibeui-date-006-past)}
[data-vibeui-block="date-006"] label{font-size:0.8125rem;font-weight:650}
[data-vibeui-block="date-006"] input{
width:100%;box-sizing:border-box;height:2.75rem;padding:0 0.75rem;
background:var(--vibeui-date-006-field);color:inherit;
border:1px solid var(--vibeui-date-006-border);border-radius:0.625rem;
font:inherit;font-size:0.9375rem;font-weight:650;font-variant-numeric:tabular-nums;
transition:border-color .16s ease;
}
[data-vibeui-block="date-006"][data-state="past"] input,
[data-vibeui-block="date-006"][data-state="soon"] input{border-color:var(--vibeui-date-006-accent)}
[data-vibeui-block="date-006"] input:focus-visible{
outline:2px solid var(--vibeui-date-006-accent);outline-offset:1px;border-color:var(--vibeui-date-006-accent);
}
[data-vibeui-block="date-006"] input::-webkit-calendar-picker-indicator{cursor:pointer;opacity:.55}
[data-vibeui-block="date-006"] input::-webkit-calendar-picker-indicator:hover{opacity:1}
/* Полоса слева вместо иконки: цвет читается боковым зрением. */
[data-vibeui-block="date-006"] [data-part="status"]{
display:flex;align-items:center;gap:0.5rem;margin:0;
padding:0.4375rem 0.625rem;border-radius:0.5rem;
border-left:3px solid var(--vibeui-date-006-accent);
background:color-mix(in oklch,var(--vibeui-date-006-accent) 10%,transparent);
font-size:0.75rem;line-height:1.4;color:var(--vibeui-date-006-fg);
}
[data-vibeui-block="date-006"] [data-part="status"] b{color:var(--vibeui-date-006-accent);font-weight:700}
[data-vibeui-block="date-006"] [data-part="status"][data-idle="true"]{
border-left-color:var(--vibeui-date-006-border);background:none;color:var(--vibeui-date-006-muted);
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="date-006"] *{animation:none!important;transition:none!important}}
`

const DAY = 86400000

// Часы устройства читаются как внешний источник: на сервере снимка нет вовсе,
// поэтому сравнивать нечего и гидратация не расходится.
const subscribeToClock = () => () => {}

function todayAtMidnight() {
  const now = new Date()
  return new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime()
}

function midnight(value: string) {
  const [year, month, day] = value.split("-").map(Number)
  if (!year || !month || !day) return null
  return new Date(year, month - 1, day).getTime()
}

function daysWord(count: number) {
  const tail = count % 10
  const teen = count % 100
  if (teen > 10 && teen < 20) return "дней"
  if (tail === 1) return "день"
  if (tail > 1 && tail < 5) return "дня"
  return "дней"
}

/**
 * Поле срока, которое предупреждает о прошедшей и о близкой дате.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Date006({
  label = "Срок сдачи",
  defaultValue = "2026-09-04",
  soonInDays = 3,
  accent,
  className,
  style,
  ...props
}: Date006Props) {
  const id = useId()
  const [value, setValue] = useState(defaultValue)
  // Сегодняшний день приходит только с клиента: на сервере снимок пустой.
  const today = useSyncExternalStore<number | null>(
    subscribeToClock,
    todayAtMidnight,
    () => null,
  )

  const target = midnight(value)
  const left =
    today !== null && target !== null
      ? Math.round((target - today) / DAY)
      : null

  const state =
    left === null
      ? "idle"
      : left < 0
        ? "past"
        : left <= soonInDays
          ? "soon"
          : "ok"

  const message =
    left === null
      ? "Срок сверяется с сегодняшним днём после загрузки."
      : left < 0
        ? `Срок прошёл ${Math.abs(left)} ${daysWord(Math.abs(left))} назад.`
        : left === 0
          ? "Срок истекает сегодня."
          : `В запасе ${left} ${daysWord(left)}.`

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

  return (
    <>
      <style href="vibeui-date-006" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="date-006"
        data-state={state}
        className={className}
        style={palette}
      >
        <label htmlFor={id}>{label}</label>
        <input
          id={id}
          type="date"
          value={value}
          aria-describedby={`${id}-status`}
          onChange={(event) => setValue(event.target.value)}
        />
        <p
          id={`${id}-status`}
          data-part="status"
          data-idle={state === "idle"}
          aria-live="polite"
        >
          {state === "past" ? <b>Просрочено.</b> : null}
          {message}
        </p>
      </div>
    </>
  )
}