Code Block

Line Callouts

A code walkthrough with callouts: marked lines get a numbered circle, and a legend under the listing explains every number in words.

  • code
  • annotation
  • docs
  • light

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-018?lang=en

middleware.ts
export const config = {  matcher: ['/dashboard/:path*'],}export function middleware(request) {  const token = request.cookies.get('session')  if (!token) return redirect('/login')}
  1. строка 2Маршруты, на которых middleware вообще запускается.
  2. строка 6Куку читаем на краю, до рендера страницы.
  3. строка 7Без токена уводим на вход и дальше не идём.
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-018" (Line Callouts) 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-018.json

Registry item: https://vibeui.ru/r/codeblock-018.json
Installs to: components/vibeui/codeblock-018.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 code walkthrough with callouts: marked lines get a numbered circle, and a legend under the listing explains every number in words.

A light listing with callouts: a numbered circle on the line and a legend with explanations below the code. Zero dependencies, no client JS.

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

<Codeblock018
  path="middleware.ts"
  showLegend={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-018-* palette — do not swap it for your theme tokens
- the circle in ::after: moved into the markup it lands in the clipboard and breaks the pasted code
- numbering the legend with a counter: hand-typed numbers eventually drift away from the callouts
- the min-height on a row — an empty line would otherwise collapse and the code loses its spacing
- the tint on a marked row along with the circle: a lone circle gets lost in a long listing
- the block's own light surface: without it the dark code disappears on a dark page

## 6. You may change
- the listing rows through the lines array
- the callouts through the notes array with a line number and a text
- the file path through path
- the legend through showLegend

## 7. Rules
- A callout number is its position in notes, not the line number: the array order is the numbering.
- A callout pointing at a missing line simply renders nothing in the code but stays in the legend.
- Keep a long explanation in the legend: the circle is sized for one or two digits.
- 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-018.json
https://vibeui.ru/r/codeblock-018.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-codeblock-018-*. Серверный: хуков нет. Кружок с номером печатает ::after через content:attr(data-note), поэтому объяснения не уезжают в буфер вместе с кодом. Номера в легенде считает CSS-счётчик от порядка элементов списка, а номера в коде берутся из той же карты — разойтись они не могут. Пустой строке задан min-height: блочный span без содержимого схлопнулся бы в ноль, и отбивка между кусками кода пропала бы. Блок светлый и несёт собственную подложку.

Component source

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

import type { CSSProperties } from "react"

export type Codeblock018Note = {
  line: number
  text: string
}

export type Codeblock018Props = {
  path?: string
  showLegend?: boolean
  lines?: string[]
  notes?: Codeblock018Note[]
  className?: string
  style?: CSSProperties
}

// Идея компонента: разбор кода, а не просто листинг. У размеченных строк
// появляется кружок с номером, а под блоком стоит легенда с пояснениями.
// Номер печатает ::after, поэтому объяснения не уезжают в буфер вместе с кодом.
const STYLES = `
:where([data-vibeui-block="codeblock-018"]){
--vibeui-codeblock-018-bg:oklch(0.99 0.003 95);
--vibeui-codeblock-018-code:oklch(0.97 0.006 95);
--vibeui-codeblock-018-fg:oklch(0.26 0.014 95);
--vibeui-codeblock-018-muted:oklch(0.52 0.012 95);
--vibeui-codeblock-018-border:oklch(0.9 0.008 95);
--vibeui-codeblock-018-mark:oklch(0.55 0.16 45);
--vibeui-codeblock-018-mark-bg:oklch(0.94 0.05 60);
--vibeui-codeblock-018-mono:ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;
--vibeui-codeblock-018-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="codeblock-018"]{
display:flex;flex-direction:column;
width:100%;max-width:34rem;box-sizing:border-box;margin:0;overflow:hidden;
border:1px solid var(--vibeui-codeblock-018-border);border-radius:0.75rem;
background:var(--vibeui-codeblock-018-bg);color:var(--vibeui-codeblock-018-fg);
font-family:var(--vibeui-codeblock-018-font);
}
[data-vibeui-block="codeblock-018"] [data-part="head"]{
padding:0.5rem 0.875rem;
border-bottom:1px solid var(--vibeui-codeblock-018-border);
font-family:var(--vibeui-codeblock-018-mono);font-size:0.75rem;
color:var(--vibeui-codeblock-018-muted);
}
[data-vibeui-block="codeblock-018"] pre{
margin:0;padding:0.75rem 0;overflow-x:auto;
background:var(--vibeui-codeblock-018-code);
}
[data-vibeui-block="codeblock-018"] code{
display:block;min-width:max-content;
font-family:var(--vibeui-codeblock-018-mono);
font-size:0.8125rem;line-height:1.85;white-space:pre;
}
/* min-height держит пустую строку: блочный span без содержимого схлопнулся
   бы в ноль и в листинге пропал бы отбивка между смысловыми кусками. */
[data-vibeui-block="codeblock-018"] [data-part="row"]{
display:block;padding:0 0.875rem;min-height:1.85em;
}
/* Кружок с номером — псевдоэлемент: в разметке он бы уехал в буфер обмена
   и сломал вставленный код. content берётся из data-note. */
[data-vibeui-block="codeblock-018"] [data-part="row"][data-note]::after{
content:attr(data-note);
display:inline-flex;align-items:center;justify-content:center;
width:1.125rem;height:1.125rem;margin-inline-start:0.625rem;
border-radius:50%;vertical-align:0.0625rem;
background:var(--vibeui-codeblock-018-mark-bg);
color:var(--vibeui-codeblock-018-mark);
font-family:var(--vibeui-codeblock-018-font);
font-size:0.6875rem;font-weight:700;line-height:1;
user-select:none;-webkit-user-select:none;
}
[data-vibeui-block="codeblock-018"] [data-part="row"][data-note]{
background:oklch(0.55 0.16 45 / 8%);
}
[data-vibeui-block="codeblock-018"] ol{
margin:0;padding:0.75rem 0.875rem;list-style:none;
counter-reset:note;
border-top:1px solid var(--vibeui-codeblock-018-border);
display:flex;flex-direction:column;gap:0.5rem;
}
[data-vibeui-block="codeblock-018"] li{
display:grid;grid-template-columns:1.125rem 1fr;gap:0.625rem;
align-items:start;font-size:0.8125rem;line-height:1.45;
}
[data-vibeui-block="codeblock-018"] li::before{
counter-increment:note;content:counter(note);
display:inline-flex;align-items:center;justify-content:center;
width:1.125rem;height:1.125rem;margin-top:0.0625rem;border-radius:50%;
background:var(--vibeui-codeblock-018-mark-bg);
color:var(--vibeui-codeblock-018-mark);
font-size:0.6875rem;font-weight:700;line-height:1;
}
[data-vibeui-block="codeblock-018"] li b{
font-family:var(--vibeui-codeblock-018-mono);font-size:0.75rem;font-weight:600;
color:var(--vibeui-codeblock-018-muted);
}
`

const LINES = [
  "export const config = {",
  "  matcher: ['/dashboard/:path*'],",
  "}",
  "",
  "export function middleware(request) {",
  "  const token = request.cookies.get('session')",
  "  if (!token) return redirect('/login')",
  "}",
]

const NOTES: Codeblock018Note[] = [
  { line: 2, text: "Маршруты, на которых middleware вообще запускается." },
  { line: 6, text: "Куку читаем на краю, до рендера страницы." },
  { line: 7, text: "Без токена уводим на вход и дальше не идём." },
]

/** Листинг с выносками к строкам и легендой под ним. */
export function Codeblock018({
  path = "middleware.ts",
  showLegend = true,
  lines = LINES,
  notes = NOTES,
  className,
  style,
}: Codeblock018Props) {
  const numbers = new Map(notes.map((note, index) => [note.line, index + 1]))

  return (
    <>
      <style href="vibeui-codeblock-018" precedence="medium">
        {STYLES}
      </style>
      <figure
        data-vibeui-block="codeblock-018"
        className={className}
        style={style}
      >
        <figcaption data-part="head">{path}</figcaption>
        <pre>
          <code>
            {lines.map((line, index) => (
              <span
                key={index}
                data-part="row"
                data-note={numbers.get(index + 1)}
              >
                {line}
              </span>
            ))}
          </code>
        </pre>
        {showLegend ? (
          <ol>
            {notes.map((note) => (
              <li key={note.line}>
                <span>
                  <b>строка {note.line}</b> — {note.text}
                </span>
              </li>
            ))}
          </ol>
        ) : null}
      </figure>
    </>
  )
}