GrCodeBlock

  • code
  • Код
  • API · 9 props
  • ⌨ клавиши

Берут, когда показать ответ сервиса как есть.

import { GrCodeBlock } from '@feugene/granularity-code/components/GrCodeBlock'

Обзор

Когда брать

  • показать ответ сервиса как есть — тело запроса, ответ модели, payload события: значение приходит unknown и структура заранее неизвестна;
  • это копируют — в тикет, в чат поддержки; кнопка копирует исходный текст, а не то, что видно на экране;
  • значение техническое — идентификаторы, хеши, конфиг: моноширинный шрифт и подсветка отвечают «это данные, а не проза»;
  • ответ длинный — maxHeight превращает блок в скроллер, достижимый с клавиатуры.

Когда взять другое

НужноБерите
Код правят, а не читаютGrCodeEditor
Сравнить две версииGrDiff
Разворачивать и сворачивать узлы данныхGrJsonViewer или GrTree (ядро)
Свернуть блок целикомGrCollapse (ядро)
Пара «характеристика → значение»GrDescriptionList (ядро)
Клавиша или сочетание в текстеGrKbd (ядро)
Значение — обычный текст, а не кодGrTextarea (ядро)

Примеры 4 у каждого — живое демо и его код

  • Доставка вебхука: запрос и ответ

    code принимает тело как объект и сериализует его с отступами; JSON — единственный язык, который блок подсвечивает сам. Кнопка копирования кладёт в буфер исходник — без номеров строк: это CSS-счётчик, а не текст, — а copy даёт странице сказать «скопировано». Ответ — простой language="text".

    invoice.paid200 OK312 ms · 6 Oct, 09:14
    Request body
    {  "id": "evt_1Q7b3x2LkS",  "type": "invoice.paid",  "created": "2026-10-06T09:14:22Z",  "data": {    "invoice": "INV-0418",    "customer": {      "id": "cus_8812",      "name": "Northwind Traders"    },    "amount": 1284.5,    "currency": "EUR",    "paid_with": "sepa_debit",    "metadata": {      "po_number": "PO-77120",      "cost_centre": null    }  },  "attempt": 1}
    Response
    HTTP/1.1 200 OKcontent-type: application/jsonx-request-id: 7f3e1c9a{"received": true}
  • Команда curl со своей подсветкой

    highlighter принимает любую функцию, которая режет текст на роли, — здесь двадцать строк регулярных выражений для shell, без Shiki и Prism. Роли названы по смыслу: флаг — keyword, $ACME_KEY — variable, и тема красит их в обоих режимах.

    Create your first invoiceReplace $ACME_KEY with a test key
    # Create a draft invoicecurl -X POST https://api.acme-billing.com/v1/invoices \  -H "Authorization: Bearer $ACME_KEY" \  -d customer=cus_8812 \  -d "items[0][price]=price_team_monthly"
  • Лог упавшего деплоя

    max-height превращает блок в окно со своей прокруткой, и длинный лог не выталкивает страницу за экран; окно — остановка Tab, и aria-label даёт ему имя. wrap переносит длинные строки: лог читают с переносами, код — как написан.

    Deploy #812 · productionFailed
    09:41:02 Cloning acme/billing-web@4f2c1e909:41:05 Installing dependencies (yarn 4.5.0, node 22.11)09:41:48 Resolved 1,284 packages in 43s09:41:49 Running build: vite build --mode production09:42:11 ✓ 2,418 modules transformed09:42:12 rendering chunks…09:42:19 dist/assets/invoice-editor-3f9a1c.js  412.70 kB │ gzip: 128.33 kB09:42:19 (!) Some chunks are larger than 400 kB after minification. Consider dynamic import() to split the application09:42:20 Running checks: bundle budget09:42:20 ✗ invoice-editor exceeds its budget: 128.3 kB gzip > 120 kB09:42:20 Build failed with exit code 1
  • Данные, на которых падает JSON.stringify

    Трекер ошибок захватывает всё, что было в памяти: сессию, ссылающуюся обратно на запрос, 64-битный идентификатор в BigInt. JSON.stringify на обоих бросает, а блок печатает [Circular] и 9007199254740993n — страница живёт, пока показывает, что сломалось.

    TypeError in send-invoiceCaptured context · 14 events in the last hour
    {  "error": "TypeError: Cannot read properties of undefined (reading 'email')",  "release": "[email protected]",  "ledgerEntryId": "9007199254740993n",  "request": {    "method": "POST",    "path": "/v1/invoices/INV-0418/send",    "session": {      "id": "sess_51c0",      "user": "[email protected]",      "plan": "Team",      "lastRequest": "[Circular]"    }  }}

API порождается из пакета · разойтись не может

Props

PropTypeпо умолчаниюОписание
languageGrCodeBlockLanguage | undefined"json"Язык подсветки: `json` и `text` разбираются встроенно, остальные — подключённой подсветкой. По умолчанию `json`.
highlighterGrCodeTokenizer | undefinedundefinedПодсветка для языков сверх встроенных. Не задана — берётся из `provide`/`inject` приложения (`GR_CODE_HIGHLIGHTER_KEY`).
maxHeightstring | number | undefinedundefinedВысота, после которой включается вертикальный скролл.
copyableboolean | undefinedundefinedКнопка копирования. Без защищённого контекста не рисуется вовсе.
wrapboolean | undefinedundefinedПереносить длинные строки вместо горизонтальной прокрутки.
lineNumbersboolean | undefinedundefinedНомера строк в колонке слева; в выделение и буфер они не попадают. Не задан — из `GrConfigProvider`, иначе `false`.
ariaLabelstring | undefinedundefinedИмя области прокрутки. Безымянную скринридер объявляет просто «регион».
size"xs" | "sm" | "md" | "lg" | undefinedundefinedСтупень размера: кегль кода и внутренние отступы. По умолчанию `md`; берётся из `GrConfigProvider`.
codeобязательныйunknown—Строка или значение: объект сериализуется с отступом.

События

EventСигнатураОписание
copy(event: "copy", code: string): void—

Типы

Все экспортируемые типы подпути, по файлам, где они объявлены. Имена, дженерики и тела читаются из файлов деклараций пакета и разойтись не могут.

Импорт
import type { GrCodeBlockLanguage, GrCodeBlockProps, GrCodeBlockEmits } from '@feugene/granularity-code/components/GrCodeBlock'

Файлdist/types/components/GrCodeBlock/config.d.ts

grCodeBlockConfigconst

Кнопка «скопировать» — GrButton ядра, и ребро объявлено с указанием провайдера: в ядре хватало имени, потому что компонент был соседом по реестру, а у спутника свой провайдер, и 'GrButton' там не резолвится. Ребро обязательно: granum подмешивает safelist и CSS только компонентам из селекции, и без него потребитель, выбравший один GrCodeBlock, получил бы кнопку без фона и без фокус-кольца.

const grCodeBlockConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrCodeBlock">;

Файлdist/types/components/GrCodeBlock/GrCodeBlock.vue.d.ts

GrCodeBlockLanguagetype

Язык подсветки. 'json' и 'text' разбирает встроенный токенизатор — без единой зависимости. Любая другая строка уходит подключённой подсветке (highlighter или провайдер приложения); её нет — строка показывается обычным текстом, и это нормальный режим, а не отказ.

type GrCodeBlockLanguage = 'json' | 'text' | (string & {});
GrCodeBlockPropsinterface

Блок сырого текста или JSON: моноширинный, с подсветкой, номерами строк и копированием в буфер. Показывает, а не редактирует и не сворачивает узлы. Свёртка по узлам — задача GrJsonViewer, дерево выбора — GrTree.

interface GrCodeBlockProps {
    /** Строка или значение: объект сериализуется с отступом. */
    code: unknown;
    /**
     * Язык подсветки: `json` и `text` разбираются встроенно, остальные — подключённой подсветкой.
     * По умолчанию `json`.
     */
    language?: GrCodeBlockLanguage;
    /**
     * Подсветка для языков сверх встроенных. Не задана — берётся из
     * `provide`/`inject` приложения (`GR_CODE_HIGHLIGHTER_KEY`).
     */
    highlighter?: GrCodeTokenizer;
    /** Высота, после которой включается вертикальный скролл. */
    maxHeight?: string | number;
    /** Кнопка копирования. Без защищённого контекста не рисуется вовсе. */
    copyable?: boolean;
    /** Переносить длинные строки вместо горизонтальной прокрутки. */
    wrap?: boolean;
    /**
     * Номера строк в колонке слева; в выделение и буфер они не попадают. Не задан — из
     * `GrConfigProvider`, иначе `false`.
     */
    lineNumbers?: boolean;
    /** Имя области прокрутки. Безымянную скринридер объявляет просто «регион». */
    ariaLabel?: string;
    /**
     * Ступень размера: кегль кода и внутренние отступы. По умолчанию `md`; берётся из
     * `GrConfigProvider`.
     */
    size?: GrComponentSize;
}
GrCodeBlockEmitsinterface
interface GrCodeBlockEmits {
    /** Текст скопирован. Тост показывает потребитель: у блока своего места для него нет. */
    (e: 'copy', code: string): void;
}

Файлdist/types/highlight/palette.d.ts

GrCodeRoletype
type GrCodeRole = typeof GR_CODE_ROLES[number];
GrCodeTokeninterface

Один разобранный кусок строки.

interface GrCodeToken {
    text: string;
    role: GrCodeRole;
}

Файлdist/types/components/GrCodeBlock/defaults.d.ts

GrCodeBlockConfigurablePropsinterface

Пропы GrCodeBlock, настраиваемые глобально через <GrConfigProvider :component-defaults="{ GrCodeBlock: { … } }">. Только оформление: code, language и ariaLabel принадлежат экземпляру. lineNumbers и wrap здесь не случайно — в админке блоки кода выглядят одинаково на всех страницах, и повторять решение у каждого значит однажды забыть.

interface GrCodeBlockConfigurableProps {
    size: GrComponentSize;
    copyable: boolean;
    wrap: boolean;
    lineNumbers: boolean;
}

Файлdist/types/components/GrCodeBlock/safelist.d.ts

grCodeBlockSafelistconst
const grCodeBlockSafelist: string[];

Доступность

Паттерн APG

блок (если скроллер) + кнопка

КлавишаДействие
—своих нет. Блок встаёт в таб-порядок (tabindex="0"), когда он скроллер **по пропам** — задан maxHeight либо выключен wrap; листается стрелками и PageUp/PageDown. Кнопка копирования — обычная кнопка, своя остановка

Использование из прозы библиотеки

Сериализация не имеет права уронить страницу

code принимает unknown, потому что данные приходят из БД и бывают любыми. Отсюда три решения, каждое из которых закрывает реальный отказ:

  • циклическая ссылка заменяется маркером [Circular], а не вешает вкладку. Маркер получает и объект, встреченный второй раз в разных ветках: обход по стеку предков разворачивал бы каждую общую ветку заново, и на «ромбе» из общих ссылок это экспонента. Неточность здесь дешевле зависания;
  • BigInt печатается с суффиксом n — штатный JSON.stringify на нём бросает;
  • всё остальное, что упало при сериализации (враждебный toJSON, геттер, который бросает), даёт [Unserializable] — на месте этого значения, а не вместо всего блока. Дерево обходится значение за значением, у каждого свой try: одно сломанное поле не прячет соседей, циклы и BigInt рядом с ним.

Строка проходит как есть, без кавычек и переформатирования: это уже готовый текст. undefined печатает пустой блок, null — литерал null; «данных нет» и «значение равно null» это разные утверждения, и решать между ними — задача страницы, а не блока.

Копируется исходник, а не экран

Кнопка кладёт в буфер ту же строку, что отрисована, — но взятую из модели, а не из разметки. Разница видна с номерами строк: скопировать вместе с ними значит получить текст, который некуда вставить.

Номера строк для этого и сделаны CSS-счётчиком: как текста их в разметке нет вовсе, поэтому они не попадают ни в буфер, ни в выделение мышью.

Без защищённого контекста кнопки нет. navigator.clipboard недоступен по http://, и кнопка, которая молча ничего не делает, хуже её отсутствия. Наличие буфера проверяется после монтирования — в первом рендере кнопки нет ни на сервере, ни на клиенте, поэтому гидрация совпадает.

Успех уходит в живой регион (useAnnouncer) и в событие copy — тост показывает потребитель: своего места для него у блока нет.

Кнопка стоит рядом со скроллером, а не поверх него. Полосу прокрутки браузер рисует у правого края <pre>, и накрывшая её кнопка отбирает те самые пиксели, за которые полосу хватают мышью. Поэтому блок резервирует справа жёлоб, а сам код сужается на его ширину; отступами это не решается — их пришлось бы взять больше ширины кнопки, и угол перестал бы быть углом. Жёлоба нет, когда нет кнопки: при copyable: false и без защищённого контекста ширина не теряется.

Скроллер и клавиатура

Блок встаёт в таб-порядок, когда он скроллер по пропам: задан maxHeight либо выключен wrap (тогда длинная строка даёт горизонтальную прокрутку). Замер переполнения не используется намеренно: он делал бы остановку Tab мигающей на каждой смене данных и на загрузке шрифта.

ariaLabel даёт области role="region" и имя. Безымянную область скринридер объявляет просто «регион», и на странице с четырьмя блоками их не различить.

Подсветка своя и только для JSON

Токенизация — чистая функция на четыре роли: ключ, строка, число, литерал. Тянуть highlight.js ради этого несоразмерно, а language="text" отключает разбор совсем.

Цвета — ссылки на роли темы, поэтому подсветка работает в светлой и тёмной без своего theme-слоя. Число взято от azure, а не от info: info — синий в двух шагах от индиго primary, и пара «ключ ↔ число» сливалась бы в {"count": 42}. Контраст и различимость ролей проверяются тестом, а не глазом.

Границы

Не редактирует, не диффит и не подсвечивает языки кроме JSON. Узлы он тоже не сворачивает — это GrJsonViewer (ядро), и различитель между ними простой: текст читают целиком или в нём ищут поле.