Display

Assignee Due Board

A board where a card answers two questions before it is opened: who is on it and when it is due. Overdue is marked with a word, and initials are tinted from the name.

  • kanban
  • assignee
  • due
  • board

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/kanban-003?lang=en

Очередь2

  • Согласовать смету по второму этапу
  • Собрать отчёт по гарантиям

В работе1

  • Перенести оплату на новый шлюз

Готово1

  • Обновить тексты писем
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 "kanban-003" (Assignee Due Board) 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/kanban-003.json

Registry item: https://vibeui.ru/r/kanban-003.json
Installs to: components/vibeui/kanban-003.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 board where a card answers two questions before it is opened: who is on it and when it is due. Overdue is marked with a word, and initials are tinted from the name.

A board with an assignee and a due date on every card: initials in a tinted circle, the date and an overdue marker. Zero dependencies, one file.

## 3. How to use it
import { Kanban003 } from "@/components/vibeui/kanban-003"

<Kanban003 today="2026-03-18" cards={[{ id: "1", title: "Estimate", assignee: "Irina Lapteva", due: "2026-03-16", column: "Queue" }]} />

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-kanban-003-* palette — do not swap it for your theme tokens
- comparing the due date against the today prop: Date.now() differs on server and client and breaks hydration
- the word "overdue" next to the date — red alone must not carry the meaning
- the initials tint from a name hash: a hand-kept palette drifts away from the team roster
- the left and right arrows on the card: dragging is unavailable from a keyboard
- the role=status live region: a move has to be audible, not only visible

## 6. You may change
- the columns and cards arrays with assignees and due dates
- today — the date the overdue check is made against
- the onChange handler and the accent through the accent prop
- the date format and the column width

## 7. Rules
- Dates are compared as strings: the format must be YYYY-MM-DD or the comparison is wrong.
- Order inside a column does not change: the move only sets membership.
- Initials come from the first two words of the name: a single word yields one letter.
- 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/kanban-003.json
https://vibeui.ru/r/kanban-003.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-kanban-003-*. Клиентский: "use client" ради состояния доски. Оттенок кружка инициалов выводится хеш-функцией из имени исполнителя и подставляется переменной, поэтому палитру людей не нужно вести руками. Срок сравнивается со строкой даты из пропа today, а не с Date.now(): текущее время на сервере и на клиенте разное и ломает гидрацию. Перенос: нативный drag мышью и стрелки влево и вправо на самой карточке с клавиатуры, результат идёт в живую область.

Component source

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

"use client"

import { useState } from "react"
import type {
  ComponentPropsWithoutRef,
  CSSProperties,
  DragEvent,
  KeyboardEvent,
} from "react"

export type Kanban003Card = {
  id: string
  title: string
  assignee: string
  due: string
  column: string
}

export type Kanban003Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children" | "onChange"
> & {
  columns?: string[]
  cards?: Kanban003Card[]
  today?: string
  onChange?: (cards: Kanban003Card[]) => void
  accent?: string
}

// Идея компонента: карточка, которая отвечает на два вопроса до открытия — кто
// делает и когда срок. Инициалы исполнителя окрашены оттенком, посчитанным из
// его имени, поэтому одинаковых кружков подряд не бывает и палитру не надо
// вести руками. Срок сравнивается со строкой даты из пропа, а не с текущим
// временем: Date.now() на сервере и на клиенте разный и ломает гидрацию.
// Просроченный срок помечен словом и знаком, а не только красным цветом.
// Перенос: мышью — drag, с клавиатуры — стрелки влево и вправо на самой
// карточке, результат объявляется в живой области.
const STYLES = `
:where([data-vibeui-block="kanban-003"]){
--vibeui-kanban-003-bg:oklch(0.985 0.002 265);
--vibeui-kanban-003-card:oklch(1 0 0);
--vibeui-kanban-003-fg:oklch(0.24 0.014 265);
--vibeui-kanban-003-muted:oklch(0.56 0.014 265);
--vibeui-kanban-003-border:oklch(0.91 0.006 265);
--vibeui-kanban-003-accent:oklch(0.55 0.2 262);
--vibeui-kanban-003-late:oklch(0.58 0.19 27);
--vibeui-kanban-003-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="kanban-003"]{
position:relative;display:grid;grid-auto-flow:column;grid-auto-columns:minmax(11.5rem,1fr);gap:0.625rem;
width:100%;box-sizing:border-box;padding:0.75rem;overflow-x:auto;
background:var(--vibeui-kanban-003-bg);
border:1px solid var(--vibeui-kanban-003-border);border-radius:0.875rem;
font-family:var(--vibeui-kanban-003-font);color:var(--vibeui-kanban-003-fg);
}
[data-vibeui-block="kanban-003"] *{box-sizing:border-box}
[data-vibeui-block="kanban-003"] [data-part="column"]{
display:flex;flex-direction:column;gap:0.5rem;min-width:0;
padding:0.5rem;border-radius:0.75rem;border:1px dashed transparent;
}
[data-vibeui-block="kanban-003"] [data-part="column"][data-over="true"]{
border-color:var(--vibeui-kanban-003-accent);
background:color-mix(in oklab,var(--vibeui-kanban-003-accent) 6%,transparent);
}
[data-vibeui-block="kanban-003"] [data-part="head"]{
margin:0;display:flex;align-items:baseline;justify-content:space-between;gap:0.5rem;
font-size:0.75rem;font-weight:650;
}
[data-vibeui-block="kanban-003"] [data-part="count"]{
color:var(--vibeui-kanban-003-muted);font-size:0.6875rem;font-variant-numeric:tabular-nums;
}
[data-vibeui-block="kanban-003"] ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:0.375rem}
[data-vibeui-block="kanban-003"] [data-part="card"]{
display:grid;gap:0.5rem;padding:0.5rem;border-radius:0.625rem;cursor:grab;
background:var(--vibeui-kanban-003-card);
border:1px solid var(--vibeui-kanban-003-border);
box-shadow:0 1px 2px oklch(0.2 0.02 265 / 6%);
font-size:0.75rem;line-height:1.35;
}
/* Карточка сама принимает фокус: перенос стрелками не требует лишних кнопок. */
[data-vibeui-block="kanban-003"] [data-part="card"]:focus-visible{
outline:2px solid var(--vibeui-kanban-003-accent);outline-offset:2px;
}
[data-vibeui-block="kanban-003"] [data-part="card"][data-dragging="true"]{opacity:.45}
[data-vibeui-block="kanban-003"] [data-part="foot"]{
display:flex;align-items:center;gap:0.375rem;
}
/* Оттенок инициалов считается из имени: палитру исполнителей не надо вести руками. */
[data-vibeui-block="kanban-003"] [data-part="who"]{
flex:none;display:grid;place-items:center;width:1.375rem;height:1.375rem;border-radius:999px;
background:color-mix(in oklab,var(--vibeui-kanban-003-hue) 18%,white);
color:color-mix(in oklab,var(--vibeui-kanban-003-hue) 78%,black);
font-size:0.5625rem;font-weight:700;line-height:1;
}
[data-vibeui-block="kanban-003"] [data-part="due"]{
margin-left:auto;display:inline-flex;align-items:center;gap:0.25rem;
color:var(--vibeui-kanban-003-muted);font-size:0.6875rem;font-variant-numeric:tabular-nums;
}
[data-vibeui-block="kanban-003"] [data-part="card"][data-late="true"] [data-part="due"]{
color:var(--vibeui-kanban-003-late);font-weight:650;
}
[data-vibeui-block="kanban-003"] [data-part="live"]{
position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
clip-path:inset(50%);white-space:nowrap;border:0;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="kanban-003"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_COLUMNS = ["Очередь", "В работе", "Готово"]

const DEFAULT_CARDS: Kanban003Card[] = [
  {
    id: "1",
    title: "Согласовать смету по второму этапу",
    assignee: "Ирина Лаптева",
    due: "2026-03-16",
    column: "Очередь",
  },
  {
    id: "2",
    title: "Собрать отчёт по гарантиям",
    assignee: "Пётр Ким",
    due: "2026-03-24",
    column: "Очередь",
  },
  {
    id: "3",
    title: "Перенести оплату на новый шлюз",
    assignee: "Мария Ковалёва",
    due: "2026-03-19",
    column: "В работе",
  },
  {
    id: "4",
    title: "Обновить тексты писем",
    assignee: "Пётр Ким",
    due: "2026-03-12",
    column: "Готово",
  },
]

function hue(name: string) {
  let hash = 2166136261

  for (const symbol of name) {
    hash ^= symbol.codePointAt(0)!
    hash = Math.imul(hash, 16777619)
  }

  return ((hash >>> 0) % 12) * 30
}

function initials(name: string) {
  return name
    .split(" ")
    .slice(0, 2)
    .map((part) => part.charAt(0).toUpperCase())
    .join("")
}

function shortDate(value: string) {
  const [, month, day] = value.split("-")
  return `${day}.${month}`
}

/**
 * Доска, где карточка называет исполнителя и срок; просрочка помечена словом.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Kanban003({
  columns = DEFAULT_COLUMNS,
  cards = DEFAULT_CARDS,
  today = "2026-03-18",
  onChange,
  accent,
  className,
  style,
  ...props
}: Kanban003Props) {
  const [board, setBoard] = useState(cards)
  const [dragged, setDragged] = useState<string | null>(null)
  const [over, setOver] = useState<string | null>(null)
  const [announcement, setAnnouncement] = useState("")

  const put = (id: string, column: string) => {
    const card = board.find((row) => row.id === id)
    if (!card || card.column === column) return

    const next = board.map((row) => (row.id === id ? { ...row, column } : row))
    setBoard(next)
    onChange?.(next)
    setAnnouncement(`«${card.title}» перенесена в «${column}».`)
  }

  const shift = (id: string, step: -1 | 1) => {
    const card = board.find((row) => row.id === id)
    if (!card) return

    const target = columns[columns.indexOf(card.column) + step]

    if (!target) {
      setAnnouncement(`«${card.title}» уже в крайней колонке «${card.column}».`)
      return
    }

    put(id, target)
  }

  const handleKey = (event: KeyboardEvent<HTMLElement>, id: string) => {
    if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return
    event.preventDefault()
    shift(id, event.key === "ArrowLeft" ? -1 : 1)
  }

  const drop = (event: DragEvent<HTMLElement>, column: string) => {
    event.preventDefault()
    setOver(null)
    if (dragged) put(dragged, column)
    setDragged(null)
  }

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

  return (
    <>
      <style href="vibeui-kanban-003" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="kanban-003"
        className={className}
        style={palette}
      >
        {columns.map((column) => {
          const rows = board.filter((card) => card.column === column)

          return (
            <section
              key={column}
              data-part="column"
              data-over={column === over}
              aria-label={`${column}: ${rows.length}`}
              onDragOver={(event) => {
                event.preventDefault()
                setOver(column)
              }}
              onDragLeave={() => setOver(null)}
              onDrop={(event) => drop(event, column)}
            >
              <p data-part="head">
                {column}
                <span data-part="count">{rows.length}</span>
              </p>
              <ul>
                {rows.map((card) => {
                  const late = card.due < today

                  return (
                    <li key={card.id}>
                      <article
                        data-part="card"
                        data-dragging={card.id === dragged}
                        data-late={late}
                        draggable
                        tabIndex={0}
                        aria-label={`${card.title}. Исполнитель ${card.assignee}. Срок ${shortDate(card.due)}${late ? ", просрочен" : ""}. Колонка «${column}». Стрелки влево и вправо переносят карточку.`}
                        style={
                          {
                            "--vibeui-kanban-003-hue": `oklch(0.62 0.16 ${hue(card.assignee)})`,
                          } as CSSProperties
                        }
                        onKeyDown={(event) => handleKey(event, card.id)}
                        onDragStart={() => setDragged(card.id)}
                        onDragEnd={() => {
                          setDragged(null)
                          setOver(null)
                        }}
                      >
                        <span>{card.title}</span>
                        <span data-part="foot">
                          <span data-part="who" aria-hidden="true">
                            {initials(card.assignee)}
                          </span>
                          <span data-part="due" aria-hidden="true">
                            {late ? "просрочен" : "до"} {shortDate(card.due)}
                          </span>
                        </span>
                      </article>
                    </li>
                  )
                })}
              </ul>
            </section>
          )
        })}
        <span data-part="live" role="status" aria-live="polite">
          {announcement}
        </span>
      </div>
    </>
  )
}