Code Block

Per Line Copy

A list of commands where a single line gets copied instead of the whole block: every row has its own button, reachable by mouse and by keyboard.

  • code
  • copy
  • list
  • commands

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/codeblock-013?lang=en

Updating a fork
  1. git remote add upstream git@github.com:vibeui/vibeui.git
  2. git fetch upstream --prune
  3. git rebase upstream/main
  4. git push --force-with-lease
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 "codeblock-013" (Per Line Copy) 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/codeblock-013.json

Registry item: https://vibeui.ru/r/codeblock-013.json
Installs to: components/vibeui/codeblock-013.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 list of commands where a single line gets copied instead of the whole block: every row has its own button, reachable by mouse and by keyboard.

A list of commands where each line is copied by its own button: the button appears on hover and on focus, and the copied row lights up. Zero dependencies.

## 3. How to use it
import { Codeblock013 } from "@/components/vibeui/codeblock-013"

<Codeblock013
  title="Updating a fork"
  showNumbers={true}
/>

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-codeblock-013-* palette — do not swap it for your theme tokens
- hiding the buttons with opacity rather than display:none — otherwise they leave the tab order
- revealing the button on :focus-visible: a hover-only interface is unusable without a mouse
- the button label carrying the line number: ten identical "Copy" labels are useless to a screen reader
- one index for the whole state: two highlighted answers at once lie about what is in the clipboard
- the dark block surface — light monospaced text is unreadable without it
- the ellipsis truncation: a scrollbar on every line turns the block into a staircase, and the full line still reaches the clipboard

## 6. You may change
- the list rows through the lines array
- the heading through title
- the numbering through showNumbers
- the copied-mark colour and how long it lives

## 7. Rules
- navigator.clipboard needs a secure context: over http the buttons stay silent.
- There is deliberately no "copy all" button: this block is about selective copying.
- A very long line scrolls inside its own cell so the button never drifts off the edge.
- 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/codeblock-013.json
https://vibeui.ru/r/codeblock-013.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-codeblock-013-*. Клиентский: директива "use client" нужна для navigator.clipboard и отметки скопированной строки. Скопированный индекс держится одним числом, поэтому одновременно подсвечена ровно одна строка. Кнопки скрыты через opacity, а не display:none, — они остаются в порядке обхода клавиатурой и проявляются по :focus-visible. Номера строк печатает CSS-счётчик, поэтому в буфер уходит только текст команды.

Component source

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

"use client"

import { useEffect, useRef, useState } from "react"
import type { CSSProperties } from "react"

export type Codeblock013Props = {
  title?: string
  lines?: string[]
  showNumbers?: boolean
  className?: string
  style?: CSSProperties
}

// Идея компонента: копировать не весь блок, а одну строку. У каждой строки
// своя кнопка: она проявляется при наведении и при фокусе с клавиатуры,
// поэтому копирование доступно и без мыши, а не только по hover.
const STYLES = `
:where([data-vibeui-block="codeblock-013"]){
--vibeui-codeblock-013-bg:oklch(0.2 0.006 265);
--vibeui-codeblock-013-head:oklch(0.24 0.008 265);
--vibeui-codeblock-013-fg:oklch(0.93 0.005 265);
--vibeui-codeblock-013-muted:oklch(0.67 0.012 265);
--vibeui-codeblock-013-gutter:oklch(0.5 0.014 265);
--vibeui-codeblock-013-border:oklch(1 0 0 / 13%);
--vibeui-codeblock-013-ok:oklch(0.84 0.14 152);
--vibeui-codeblock-013-mono:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;
--vibeui-codeblock-013-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="codeblock-013"]{
display:flex;flex-direction:column;
width:100%;max-width:32rem;box-sizing:border-box;margin:0;overflow:hidden;
border:1px solid var(--vibeui-codeblock-013-border);border-radius:0.75rem;
background:var(--vibeui-codeblock-013-bg);color:var(--vibeui-codeblock-013-fg);
font-family:var(--vibeui-codeblock-013-font);
}
[data-vibeui-block="codeblock-013"] figcaption{
padding:0.5rem 0.875rem;
background:var(--vibeui-codeblock-013-head);
border-bottom:1px solid var(--vibeui-codeblock-013-border);
font-family:var(--vibeui-codeblock-013-mono);font-size:0.75rem;
color:var(--vibeui-codeblock-013-muted);
}
[data-vibeui-block="codeblock-013"] ol{
margin:0;padding:0.375rem 0;list-style:none;
counter-reset:line;
}
[data-vibeui-block="codeblock-013"] li{
display:flex;align-items:center;gap:0.5rem;
padding:0 0.375rem 0 0.875rem;min-height:1.625rem;
transition:background-color .16s ease;
}
[data-vibeui-block="codeblock-013"] li[data-numbered="true"]::before{
counter-increment:line;content:counter(line);
flex:none;width:1.5rem;text-align:right;
font-family:var(--vibeui-codeblock-013-mono);font-size:0.75rem;
color:var(--vibeui-codeblock-013-gutter);font-variant-numeric:tabular-nums;
user-select:none;-webkit-user-select:none;
}
[data-vibeui-block="codeblock-013"] li:hover{background:oklch(1 0 0 / 6%)}
[data-vibeui-block="codeblock-013"] li[data-copied="true"]{background:oklch(0.6 0.14 152 / 18%)}
/* Строка режется многоточием, а не прокручивается: своя полоса прокрутки на
   каждой строке превращает блок в лестницу, а кнопка копирования уезжает за
   край. Целиком строка всё равно уходит в буфер. */
[data-vibeui-block="codeblock-013"] code{
flex:1 1 auto;min-width:0;
font-family:var(--vibeui-codeblock-013-mono);
font-size:0.8125rem;line-height:1.6;
white-space:pre;overflow:hidden;text-overflow:ellipsis;
}
[data-vibeui-block="codeblock-013"] button{
appearance:none;border:0;cursor:pointer;flex:none;
width:1.5rem;height:1.5rem;border-radius:0.375rem;
display:inline-flex;align-items:center;justify-content:center;
background:oklch(1 0 0 / 10%);color:var(--vibeui-codeblock-013-muted);
opacity:0;transition:opacity .16s ease,color .16s ease;
}
[data-vibeui-block="codeblock-013"] li:hover button,
[data-vibeui-block="codeblock-013"] button:focus-visible,
[data-vibeui-block="codeblock-013"] button[data-copied="true"]{opacity:1}
[data-vibeui-block="codeblock-013"] button:hover{color:var(--vibeui-codeblock-013-fg)}
[data-vibeui-block="codeblock-013"] button:focus-visible{outline:2px solid var(--vibeui-codeblock-013-ok);outline-offset:2px}
[data-vibeui-block="codeblock-013"] button[data-copied="true"]{color:var(--vibeui-codeblock-013-ok)}
[data-vibeui-block="codeblock-013"] svg{width:0.875rem;height:0.875rem}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="codeblock-013"] *{animation:none!important;transition:none!important}}
`

const LINES = [
  "git remote add upstream git@github.com:vibeui/vibeui.git",
  "git fetch upstream --prune",
  "git rebase upstream/main",
  "git push --force-with-lease",
]

/** Список строк, каждую можно скопировать отдельной кнопкой. */
export function Codeblock013({
  title = "Обновление форка",
  lines = LINES,
  showNumbers = true,
  className,
  style,
}: Codeblock013Props) {
  const [copied, setCopied] = useState(-1)
  const timer = useRef<ReturnType<typeof setTimeout> | null>(null)

  useEffect(
    () => () => {
      if (timer.current) clearTimeout(timer.current)
    },
    [],
  )

  const copy = async (line: string, index: number) => {
    try {
      await navigator.clipboard.writeText(line)
    } catch {
      // Буфер недоступен: отметка «скопировано» была бы обманом.
      return
    }
    setCopied(index)
    if (timer.current) clearTimeout(timer.current)
    timer.current = setTimeout(() => setCopied(-1), 1400)
  }

  return (
    <>
      <style href="vibeui-codeblock-013" precedence="medium">
        {STYLES}
      </style>
      <figure
        data-vibeui-block="codeblock-013"
        className={className}
        style={style}
      >
        <figcaption>{title}</figcaption>
        <ol>
          {lines.map((line, index) => (
            <li
              key={index}
              data-numbered={showNumbers || undefined}
              data-copied={copied === index || undefined}
            >
              <code>{line}</code>
              <button
                type="button"
                data-copied={copied === index || undefined}
                onClick={() => copy(line, index)}
                aria-label={
                  copied === index
                    ? `Строка ${index + 1} скопирована`
                    : `Скопировать строку ${index + 1}`
                }
              >
                {copied === index ? (
                  <svg viewBox="0 0 12 12" aria-hidden="true">
                    <path
                      d="M2 6.5 4.6 9 10 3"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth="1.8"
                      strokeLinecap="round"
                      strokeLinejoin="round"
                    />
                  </svg>
                ) : (
                  <svg viewBox="0 0 12 12" aria-hidden="true">
                    <rect
                      x="4"
                      y="1.5"
                      width="6.5"
                      height="6.5"
                      rx="1.5"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth="1.3"
                    />
                    <path
                      d="M8 10.5H3a1.5 1.5 0 0 1-1.5-1.5V4"
                      fill="none"
                      stroke="currentColor"
                      strokeWidth="1.3"
                      strokeLinecap="round"
                    />
                  </svg>
                )}
              </button>
            </li>
          ))}
        </ol>
      </figure>
    </>
  )
}