npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pgcorp/ui-kit

v0.19.0

Published

Typed Vue 3 design system with accessible components, semantic themes, and workbench patterns.

Readme

@pgcorp/ui-kit

Типизированная дизайн-система PGCorp для Vue 3: от базовых controls до полноценных workbench-интерфейсов.

A typed PGCorp design system for Vue 3, spanning foundational controls and complete workbench interfaces.

Vue 3 · TypeScript · exact public exports · 6 themes · MIT

Что входит / What is included

  • Vue Single-File Components с типизированными props, slots, events и exposed methods. Vue Single-File Components with typed props, slots, events, and exposed methods.
  • Controls, navigation, containers, data display, overlays и layout-примитивы. Controls, navigation, containers, data display, overlays, and layout primitives.
  • Таблицы, деревья, редакторы, graph surfaces и составные workbench-компоненты. Tables, trees, editors, graph surfaces, and composed workbench components.
  • Semantic CSS tokens и шесть светлых и тёмных тем. Semantic CSS tokens and six light and dark themes.
  • Контракты keyboard navigation, focus, ARIA, loading, disabled, invalid и error states. Contracts for keyboard navigation, focus, ARIA, loading, disabled, invalid, and error states.
  • Только явные package exports: wildcard и private source imports отсутствуют. Explicit package exports only, with no wildcards or private source imports.

Закрываемые вкладки и SSR / Closable tabs and SSR

STab #close, #actions и #editor сохраняют Vue owner и состояние через Teleport в keyed targets вне role=tablist. Tab triggers принадлежат одному semantic tablist; auxiliary controls используют его общий scroll/layout owner. Editing сохраняет настоящий disabled tab и размещает редактор вне tablist. Props/slots задают reserve/overlay и hover/always presentation; touch не требует hover. Родитель подтверждает удаление и выбирает активную вкладку. Keyed-перестановка сохраняет mounted auxiliary control, его черновик, выделение и текущий keyboard focus; native Tab order соответствует порядку участников. Focused auxiliary host остаётся подключённым при перестановке соседей: native blur/focus events не подавляются, программный refocus не выполняется. Намеренный внешний focus, blur и focus нового popup сохраняют свою обычную семантику.

SSR-приложение вызывает assembleUiKitSsrTeleports из @pgcorp/ui-kit/ssr после renderToString, затем размещает оставшиеся host Teleports. Контракт сборки и гидратации описан в docs/ssr.md.

Public auxiliary slots preserve their Vue owner and local state outside the semantic tablist. SSR applications assemble package-owned Teleports through @pgcorp/ui-kit/ssr and retain responsibility for their host Teleports. Keyed reordering retains the mounted auxiliary control, draft, selection, and current keyboard focus while native Tab order follows participant order. The focused auxiliary host remains connected while its siblings move; native blur/focus events are not suppressed and no programmatic refocus is performed. Intentional external focus, blur, and a new popup's focus retain their normal semantics. See the SSR contract for assembly and hydration.

Полные option-каталоги / Complete option catalogs

SListbox.virtualization задаёт measured viewport над полным набором options: { viewportHeight, estimatedItemHeight, overscan? }. Keyboard navigation, groups, clear, disabled и multiple selection работают по всему каталогу. Для external focus owner событие active-descendant-change передаёт ID смонтированной option в STextarea.suggestions. Полный контракт и примеры включены в пакет: docs/listbox.md.

SListbox.virtualization provides a measured viewport over the complete option catalog. Keyboard navigation and selection use the full inventory; the active-descendant-change event supplies a mounted option ID to an external focus owner. The package includes the complete contract and examples in docs/listbox.md.

Измеряемый прогресс / Measured progress

SProgressBar использует один numerical/accessibility owner для shape="linear" и shape="circular". Линейная форма является default. Круговая форма имеет размеры xs/sm/md = 24/28/32 px и требует явный value от 0 до 100 в mode="determinate". showValue выводит округлённое число в центре; aria-valuenow сохраняет точное значение, включая дробную часть. valueText передаёт доступное предметное описание, например tokens/limit. Существующий scoped default slot форматирует только linear label и получает точный value; circular label принадлежит владельцу и отклоняет default slot.

accuracy="estimated" требует valueText и показывает приблизительность. mode="unknown" требует valueText, не принимает value и не сообщает aria-valuenow. Эти состояния статичны. mode="indeterminate" обозначает выполняющуюся операцию, не принимает showValue и учитывает reduced motion. severity управляет semantic palette, а не выводится из процента автоматически.

<SProgressBar
  :accessibility="{ mode: 'label', label: 'Заполнение контекста' }"
  shape="circular"
  size="md"
  :value="37.5"
  value-text="37 500 из 100 000 токенов"
  show-value
  severity="info"
/>
<SProgressBar
  :accessibility="{ mode: 'label', label: 'Заполнение контекста' }"
  shape="circular"
  mode="unknown"
  value-text="Размер контекста недоступен"
  show-value
/>

Для действия показатель помещается в default slot SButton с iconOnly, shape="circle", size="lg", density="default" и appearance="ghost": штатная контрольная область равна 48×48 px, то есть не меньше 44 px. Внутренний показатель использует accessibility.mode="decorative"; действие, доступное имя с точным значением и tooltip bindings принадлежат внешней кнопке. Показатель не создаёт вложенный control или дополнительную остановку Tab.

SProgressBar shares numerical and accessibility ownership across linear and circular shapes. Circular xs/sm/md sizes are 24/28/32 px. Determinate circular progress requires an explicit value in 0–100; central display rounds the percentage while aria-valuenow retains its exact precision. valueText supplies a domain description such as tokens/limit. Estimated progress requires valueText and a visible approximation marker. Explicit unknown progress requires valueText, rejects value, omits aria-valuenow, and remains static. Severity is consumer-selected rather than inferred from a percentage. The existing scoped default slot formats linear labels and receives the exact value. Circular labels are owner-controlled and reject a supplied default slot.

For an actionable indicator, place decorative progress in the default slot of a circular SButton with iconOnly, size="lg", default density, and appearance="ghost": its target is 48×48 px, not exactly 44 px. The inner progress is decorative; the button owns focus, the action, the accessible name, and tooltip bindings.

Установка / Installation

npm install @pgcorp/ui-kit @vue/devtools-api vue pinia vue-router

| Требование / Requirement | Версия / Version | | --- | --- | | Node.js | ^22.22.2 \|\| ^24.15.0 \|\| ^26.0.0 | | npm | >=10.9.2 <12 | | Vue | ^3.5.43 | | Pinia | ^4.0.3 | | Vue Devtools API | ^8.2.1 | | Vue Router | ^5.3.1 | | Build tool | Vite with Vue SFC support |

vue, pinia, @vue/devtools-api и vue-router являются peer dependencies. Consumer-приложение владеет их единственными runtime-экземплярами.

vue, pinia, @vue/devtools-api, and vue-router are peer dependencies. The consumer application owns their single runtime instances.

Tailwind/PostCSS принадлежат build-контуру приложения и объявляются его прямыми dev dependencies. / Tailwind/PostCSS belong to the application's build pipeline and must be declared as its direct dev dependencies.

npm install -D tailwindcss@^4.3.3 @tailwindcss/postcss@^4.3.3 postcss@^8.5.28

postcss.config.mjs:

export default { plugins: { '@tailwindcss/postcss': {} } }

Контракт установки: Tailwind CSS с PostCSS. / Setup contract: Tailwind CSS with PostCSS.

Быстрый старт / Quick start

Создайте корневой CSS entrypoint приложения. Интеграционный stylesheet подключает стили и typography plugin, а явный @source регистрирует собранные модули пакета:

Create the application's root CSS entrypoint. The integration stylesheet includes the styles and typography plugin, while an explicit @source registers the built package modules:

@import "tailwindcss";
@import "@pgcorp/ui-kit/tailwind.css";
@source "../node_modules/@pgcorp/ui-kit";

@source обязателен: Tailwind 4 не сканирует dependencies автоматически, а путь разрешается относительно CSS-файла приложения. Runtime-импорт reference.css не требуется. / @source is required because Tailwind 4 does not scan dependencies automatically; the path resolves relative to the application CSS. No runtime reference.css import is required.

Подключите CSS, Pinia и router в entrypoint приложения:

Import the CSS, Pinia, and router from the application entrypoint:

import { createPinia } from 'pinia'
import { createApp } from 'vue'
import './styles.css'

import App from './App.vue'
import router from './router'

createApp(App)
  .use(createPinia())
  .use(router)
  .mount('#app')

Импортируйте каждый компонент через точный публичный subpath:

Import every component through its exact public subpath:

<script setup lang="ts">
import SPanel from '@pgcorp/ui-kit/shared/containers/SPanel.vue'
import SButton from '@pgcorp/ui-kit/shared/controls/SButton.vue'
</script>

<template>
  <SPanel
    title="Профиль"
    description="Настройки рабочей области"
    :heading-level="2"
    content-layout="stack"
    content-gap="content"
  >
    <SButton appearance="solid" severity="primary">
      Сохранить
    </SButton>
  </SPanel>
</template>

Production build должен разрешать package exports напрямую, без alias на исходный monorepo и без копирования файлов UI-kit.

The production build must resolve package exports directly, without a source monorepo alias or copied UI-kit files.

Локализация / Localization

UI-kit поддерживает en, ru, zh-CN и zh-TW. Без provider используется ru; SUiKitLocaleProvider задаёт локаль для одного Vue subtree и не создаёт дополнительный DOM-элемент. / The UI kit supports en, ru, zh-CN, and zh-TW. Without a provider it uses ru; SUiKitLocaleProvider selects a locale for one Vue subtree without adding a DOM element.

<script setup lang="ts">
import SUiKitLocaleProvider from '@pgcorp/ui-kit/shared/containers/SUiKitLocaleProvider.vue'
import type { UiKitLocale } from '@pgcorp/ui-kit/composables/useUiKitLocale'

const locale: UiKitLocale = 'en'
</script>

<template>
  <SUiKitLocaleProvider :locale="locale">
    <AppWorkspace />
  </SUiKitLocaleProvider>
</template>

Provider переводит только kit-owned visible/assistive copy. Доменные данные и пользовательский контент остаются ответственностью consumer. Вложенные provider наследуют ближайший context и принимают типизированный messages override. / The provider translates kit-owned visible and assistive copy only. Domain data and user content remain consumer-owned. Nested providers inherit the nearest context and accept a typed messages override.

Уведомления / Notifications

SControlledToastViewport отображает readonly application records и возвращает serializable dismiss, action и lifetime-expired intents. Consumer остаётся владельцем массива, порядка, ID и durable history. Режим consumer не создаёт таймеры; режим viewport управляет только presentation lifetime и ставит его на паузу при hover/focus. / SControlledToastViewport renders readonly application records and emits serializable dismiss, action, and lifetime-expired intents. The consumer owns the array, ordering, IDs, and durable history. The consumer mode creates no timers; the viewport mode owns presentation lifetime only and pauses it on hover/focus.

import type { SToastPresentation } from '@pgcorp/ui-kit/shared/data-display/toast'
import SControlledToastViewport from '@pgcorp/ui-kit/shared/data-display/SControlledToastViewport.vue'

SUiKitNotifierProvider маршрутизирует внутренний feedback SCodeBlock, SCopyField, SExpandableText и SDocBlock в typed application-owned sink. Provider не создаёт DOM, Pinia store, IDs, history или timers; async publisher ожидается вызывающим действием. Host связывает intent со своим publisher и SControlledToastViewport. / SUiKitNotifierProvider routes internal feedback from SCodeBlock, SCopyField, SExpandableText, and SDocBlock to a typed, application-owned sink. It creates no DOM, Pinia store, IDs, history, or timers; the calling action awaits an asynchronous publisher. The host connects intents to its publisher and SControlledToastViewport.

import SUiKitNotifierProvider from '@pgcorp/ui-kit/shared/containers/SUiKitNotifierProvider.vue'
import type { SUiKitNotificationIntent, SUiKitNotificationSink } from '@pgcorp/ui-kit/composables/useNotifier'

Контекст выбирается в setup, ближайший provider владеет маршрутом; смена sink действует на следующее уведомление. Контракт и интеграция / Contract and integration. Context is selected during setup; the nearest provider owns routing, and sink replacement applies to the next notification.

При отсутствии provider SToastContainer и useNotifier образуют автономный convenience-контракт для одного Pinia root. Store хранит не более 100 history records и освобождает свои таймеры через $dispose()/$reset(). / Without a provider, SToastContainer and useNotifier provide the autonomous convenience contract for one Pinia root. The store keeps at most 100 history records and releases its timers through $dispose()/$reset().

Безопасные rich documents / Safe rich documents

SRichDocument отображает закрытую versioned SRichDocumentV1 AST без raw HTML, VNodes, callbacks и trust flags. Внешний JSON проверяется через validateRichDocument и validateRichDocumentResources; ошибки содержат stable code и точный node path. Consumer-owned resources связываются по ID, а link/resource/action effects возвращаются как typed intents. Link-node с current: true выражает текущее назначение через aria-current="page". Task lists, deletion, keyboard, sup/sub, disclosure и scoped footnotes имеют собственные serializable nodes; raw markup для этих семантик не требуется. Типизированный semantic profile включает hard breaks, inline/picture media, rich summary/header content, definitions, ruby и структурные таблицы с header associations, spans и footer. STable, SDataGrid и rich tables используют общих native table owners. Якоря и повторные сноски имеют локальный scope, раскрывают целевую секцию и переводят фокус без изменения истории. heading.presentation: 'assistive' сохраняет нативный уровень, содержимое, scoped anchors и IDREFs, визуально скрывая сам heading вне normal flow. Значение по умолчанию — visible; интерактивные потомки в assistive heading отклоняются с точным node path. GFM producer сохраняет скрытый heading сносок и его describedBy references, не передавая raw class. Вложенные блоки адаптируются к ширине контейнера при resize. Длинные строки code fences имеют локальную горизонтальную прокрутку; viewport document сохраняет полную высоту кода.

SRichDocument renders the closed, versioned SRichDocumentV1 AST without raw HTML, VNodes, callbacks, or trust flags. External JSON is checked with validateRichDocument and validateRichDocumentResources; failures include a stable code and exact node path. Consumer-owned resources are joined by ID, and link/resource/action effects are emitted as typed intents. A link node with current: true exposes the current destination through aria-current="page". Task lists, deletion, keyboard, sup/sub, disclosure, and scoped footnotes use dedicated serializable nodes; these semantics never require raw markup. The typed semantic profile includes hard breaks, inline/picture media, rich summary/header content, definitions, ruby, and structural tables with header associations, spans, and a footer. STable, SDataGrid, and rich tables share native table owners. Anchors and repeated footnotes are scoped locally, reveal their target disclosure, and move focus without changing browser history. heading.presentation: 'assistive' retains the native level, content, scoped anchors, and IDREFs while placing the heading outside normal flow. The default is visible; interactive descendants fail at their exact node path. A GFM producer preserves its hidden footnote heading and description references without passing raw classes. Nested blocks adapt to the container width on resize. Long code-fence lines scroll horizontally within the block; the document viewport preserves the complete code height.

SCheckbox presentation="inline" и SField contentFlow="phrasing" сохраняют phrasing content model внутри текста. SLink предоставляет actual native getElement()/focus() и semantic props domId, language, direction, ariaLabel, ariaLabelledby, ariaDescribedby; IDREFs задаются непустыми DOM идентификаторами через один ASCII-пробел без автоматического исправления ввода. / SCheckbox presentation="inline" and SField contentFlow="phrasing" preserve the phrasing content model inside text. SLink exposes the actual native getElement()/focus() and the semantic props listed above; IDREFs are explicit non-empty DOM identities separated by one ASCII space, without input repair.

import SRichDocument from '@pgcorp/ui-kit/shared/data-display/SRichDocument.vue'
import {
  validateRichDocument,
  validateRichDocumentResources,
  type SRichDocumentV1,
} from '@pgcorp/ui-kit/shared/data-display/richDocument'

Code highlighting and KaTeX load through local lazy chunks; SSR uses safe text fallbacks. Полный security, resource, event и versioning contract описан в docs/rich-document.md. / The complete security, resource, event, and versioning contract is documented in docs/rich-document.md.

Области прокрутки / Scroll viewports

SScrollViewport владеет именованной scroll областью, осями x/y/both, semantic block size, overscroll policy и root-only keyboard navigation. Событие scroll-state возвращает normalized physical и writing-mode-aware semantic offsets. Exposed API предоставляет focus, scrollToBoundary и reveal, но не открывает DOM owner.

semanticRole="region" — default именованного landmark; group задаёт вложенную область без landmark, в том числе повторяемые журналы с одинаковыми именами. focusPolicy="always" сохраняет выбранную роль и имя; focusPolicy="overflow" добавляет их и Tab stop только при переполнении разрешённой оси. Имя задаётся ровно одним из accessibleLabel и accessibleLabelledby. blockSize="content" сохраняет естественную высоту, maxBlockSize="none|sm|md|lg" ограничивает её общей шкалой viewport. Таблицы STable, SDataGrid и rich-document используют этот общий scroll owner: пустые и пассивные таблицы доступны клавиатуре при переполнении, вложенные controls сохраняют собственную клавиатуру. При исчезновении переполнения focused root сохраняет tabindex="-1" до blur; он не добавляет остановку Tab и не переносит фокус.

SScrollViewport owns a named scroll area, x/y/both axes, semantic block size, overscroll policy, and root-only keyboard navigation. The scroll-state event provides normalized physical and writing-mode-aware semantic offsets. Its exposed API provides focus, scrollToBoundary, and reveal without exposing the DOM owner.

semanticRole="region" is the default named landmark; group represents a nested non-landmark area, including repeated logs with identical names. focusPolicy="always" retains the selected role and name; focusPolicy="overflow" adds them and a Tab stop only when an enabled axis overflows. Exactly one of accessibleLabel and accessibleLabelledby supplies its name. blockSize="content" retains intrinsic height; maxBlockSize="none|sm|md|lg" uses the shared viewport limit scale. STable, SDataGrid, and rich-document tables compose this scroll owner: overflowing empty and passive tables are keyboard reachable, while nested controls retain their own keyboard behavior. When overflow disappears, a focused root retains tabindex="-1" until blur; it adds no Tab stop and does not move focus.

import SScrollViewport from '@pgcorp/ui-kit/shared/data-display/SScrollViewport.vue'
import type {
  SScrollViewportApi,
  SScrollViewportSemanticRole,
  SScrollViewportState,
} from '@pgcorp/ui-kit/shared/data-display/scrollViewport'

Вложенные inputs, editors и terminals сохраняют собственную клавиатуру, selection и native copy. Полный axes, state, API и lifecycle contract описан в docs/scroll-viewport.md. / Nested inputs, editors, and terminals retain their keyboard behavior, selection, and native copy. The complete axes, state, API, and lifecycle contract is documented in docs/scroll-viewport.md.

Карта компонентов / Component map

| Группа / Group | Назначение / Purpose | Примеры exports / Example exports | | --- | --- | --- | | Layout | App shell, dock regions, stacks, workbench layout | layout/SAppShell.vue, layout/SStack.vue, layout/SWorkbenchLayout.vue | | Controls | Buttons, fields, inputs, switches, selects, comboboxes, menus | shared/controls/SButton.vue, shared/controls/SSwitch.vue, shared/controls/SCombobox.vue | | Containers | Panels, modal, drawer, popover, sidebars | shared/containers/SPanel.vue, shared/containers/SModal.vue, shared/containers/SPopover.vue | | Data display | Tables, charts, metric cards, scroll regions, chips, status, progress, code, JSON, tooltips | shared/data-display/STable.vue, shared/data-display/SChart.vue, shared/data-display/SScrollViewport.vue | | Navigation | Tabs, breadcrumbs, catalog navigation, wizard | shared/navigation/STabs.vue, shared/navigation/SBreadcrumbs.vue, shared/navigation/SWizardSteps.vue | | Complex surfaces | Tree, Kanban, data grid, SQL editor, graph | shared/complex/STree.vue, shared/database/SDataGrid.vue, shared/graph/SGraphViewport.vue | | Runtime | Themes, variants, icons, composables, stores | theme, variants, icons, composables/useNotifier, stores/useNotifierStore |

Полный машинно-читаемый каталог находится в package.json#exports. IDE и TypeScript показывают доступные subpaths при импорте.

The complete machine-readable catalog is declared in package.json#exports. IDEs and TypeScript expose the available subpaths during import.

Реестры и свободный ввод / Registries and custom values

STable владеет служебными колонками выбора и раскрытия. Consumer передаёт controlled keys и доменный контент, не копирует checkbox, disclosure или CSS:

STable owns selection and disclosure columns. Consumers provide controlled keys and domain content instead of copying checkboxes, disclosures, or CSS:

import type {
  STableColumn,
  STableSort,
} from '@pgcorp/ui-kit/shared/data-display/table'
<STable
  v-model:selected-row-keys="selectedRowKeys"
  v-model:expanded-row-keys="expandedRowKeys"
  :columns="columns"
  :data="rows"
  :get-row-key="(row) => row.id"
  :get-row-label="(row) => row.name"
  selection-mode="multiple"
  aria-label="Поставщики"
  @row-contextmenu="openRowMenu"
>
  <template #row-details="{ row }">
    <SupplierDetails :supplier="row" />
  </template>
</STable>

Для служебных столбцов доступны headerPresentation: 'assistive', width: '2xs' и width: 'content'. SCombobox принимает новое строковое значение через allow-custom-value; SPageHeader сочетает appearance="bare" с title-presentation="assistive" для доступного заголовка без визуальной полосы.

Utility columns support headerPresentation: 'assistive', width: '2xs', and width: 'content'. SCombobox accepts a new string value with allow-custom-value; SPageHeader combines appearance="bare" with title-presentation="assistive" for an accessible zero-chrome heading.

Составная сортировка использует controlled v-model:sorts: consumer задаёт sort-mode="multiple" и передаёт ordered STableSort[]. Клик по заголовку добавляет критерий, меняет направление или удаляет его; номер возле заголовка показывает приоритет. Таблица не переставляет строки самостоятельно.

Multi-column sorting uses controlled v-model:sorts: the consumer sets sort-mode="multiple" and provides an ordered STableSort[]. Header activation adds a descriptor, changes its direction, or removes it; the visible number is the priority. The table never reorders rows on its own.

Последовательные действия заголовков формируют внутренний intent draft до подтверждения controlled prop. Consumer может применить sort/sorts через store или асинхронный render-cycle: второй критерий не теряет первый. Любое authoritative обновление prop становится новой базой следующих действий.

Sequential header actions update an internal intent draft until the controlled prop is acknowledged. Consumers may apply sort or sorts through a store or an asynchronous render cycle without losing an earlier criterion. Any authoritative prop update becomes the base for subsequent actions.

<STable
  v-model:sorts="sorts"
  :columns="columns"
  :data="sortedRows"
  :get-row-key="(row) => row.id"
  sort-mode="multiple"
  aria-label="Позиции прайс-листа"
/>

Графики и метрики / Charts and metrics

SChart является единым semantic owner для line, area, grouped/stacked bar, pie и donut. Размеры compact, sm, md, lg, оси, сетка, легенда, маркеры, подписи, auto/fixed domain и таблица данных задаются typed props. Компонент пересчитывает SVG-геометрию по фактическому контейнеру, сохраняет читаемую типографику и переносит длинные подписи категорий на узкой ширине.

SChart is the single semantic owner for line, area, grouped/stacked bar, pie, and donut charts. Typed props control compact, sm, md, and lg sizes, axes, grid, legend, markers, labels, auto/fixed domains, and the data table. The component recomputes SVG geometry for its actual container, preserves readable typography, and wraps long category labels at narrow widths.

Форматированные подписи шкалы Y участвуют в расчёте поля графика. Крайние подписи X привязаны к границам plot-area, поэтому x-labels="auto" сохраняет полные начальное и конечное значения и не обрезает их краем SVG. Если ширины контейнера физически недостаточно, визуальная подпись сокращается, а полное значение остаётся в tooltip и доступной таблице данных.

Formatted Y-axis labels participate in plot-gutter calculation. Endpoint X labels are anchored to the plot boundaries, so x-labels="auto" preserves the full first and last values instead of clipping them at the SVG edge. When the container is physically too narrow, the visual label is shortened while the full value remains available through the tooltip and accessible data table.

import type {
  SChartCategory,
  SChartSeries,
} from '@pgcorp/ui-kit/shared/data-display/chart'
<SChart
  type="area"
  :categories="categories"
  :series="series"
  :accessibility="{
    mode: 'label',
    label: 'Нагрузка VPN по часам',
    description: 'Входящий и исходящий трафик',
  }"
  x-axis-label="Час"
  y-axis-label="Мбит/с"
  legend="top"
/>

Для компактного тренда SMetricCard предоставляет слот #chart. Декоративный график внутри уже озвученной карточки использует accessibility.mode="decorative" и data-table-presentation="none"; содержательный самостоятельный график сохраняет default assistive data table.

SMetricCard exposes #chart for a compact trend. A decorative chart inside an already labelled card uses accessibility.mode="decorative" with data-table-presentation="none"; a meaningful standalone chart keeps the default assistive data table.

Сравнение исходников / Source diff

SDiffViewer является semantic owner для exact-text diff. before.text и after.text сравниваются без нормализации переводов строк или содержимого; label, revision и language являются metadata. layout="unified" сохраняет единый горизонтальный scroll для длинных строк, layout="split" показывает равные колонки с переносом кода. viewport использует те же semantic размеры, что и code surfaces.

SDiffViewer is the semantic owner for exact-text diff. before.text and after.text are compared without newline or payload normalization; label, revision, and language are metadata. layout="unified" preserves one horizontal scroll surface for long lines, while layout="split" renders equal columns with wrapped code. viewport uses the same semantic sizes as the code surfaces.

import type {
  SDiffResult,
  SDiffSource,
} from '@pgcorp/ui-kit/shared/data-display/diff'
import SDiffViewer from '@pgcorp/ui-kit/shared/data-display/SDiffViewer.vue'
<SDiffViewer
  v-model:expanded-gap-ids="expandedGapIds"
  :before="baseSource"
  :after="currentSource"
  :context-lines="3"
  layout="split"
  viewport="expanded"
  aria-label="Изменения конфигурации"
  @computed="handleComputed"
  @retry="handleRetry"
/>

contextLines задаёт число видимых неизменённых строк вокруг каждого hunk. Без expandedGapIds компонент владеет раскрытием gap; с v-model:expanded-gap-ids consumer применяет intent как controlled state. computed передаёт валидированный SDiffResult, retry сообщает о повторном запуске после ошибки. Для синхронного вычисления вне интерактивной поверхности экспортируется computeDiff(before, after, options).

contextLines selects the unchanged lines around each hunk. Without expandedGapIds, the component owns gap expansion; with v-model:expanded-gap-ids, the consumer applies expansion intents as controlled state. computed provides a validated SDiffResult, and retry reports a retry after an error. computeDiff(before, after, options) is exported for synchronous computation outside an interactive surface.

Строки виртуализируются независимо от размера source. Дорогие интерактивные вычисления выполняются через управляемый worker: смена source отменяет и освобождает прежнюю работу, unmount освобождает активный worker, а результат неактуального поколения не попадает в UI. Consumer не создаёт worker и не управляет его жизненным циклом.

Lines are virtualized independently of source size. Expensive interactive computations use a managed worker: source replacement cancels and releases the previous job, unmount releases the active worker, and stale-generation results never reach the UI. Consumers neither create the worker nor manage its lifecycle.

Действия chip и адаптивный header панели / Chip actions and responsive panel header

SChip принимает typed surface contract для статичного, action и link режимов и эмитит activate. Закрытие остаётся отдельным sibling-действием, не вложенной кнопкой. SPanel использует header-layout="auto" для переноса группы действий под полный заголовок, если собственная ширина heading/actions и semantic gap не помещаются в content width шапки. При достаточном месте обе группы остаются inline; фиксированного breakpoint нет. Описание переносится в выделенной heading ширине и не задаёт intrinsic ширину группы. Действия внутри группы могут переноситься; inline сохраняет одну внешнюю строку, а stacked всегда размещает действия под заголовком. SLeftSidebar.headerLayout передаёт тот же режим SPanel, с default auto; компактная шапка с одной action использует inline без consumer CSS.

SChip accepts the typed surface contract for static, action, and link modes and emits activate. Closing remains a separate sibling action rather than a nested button. SPanel uses header-layout="auto" to move actions below the complete title when the intrinsic heading/action widths and semantic gap do not fit the header's content width. Both groups remain inline when they fit; there is no fixed breakpoint. The description wraps within the allocated heading width without defining its intrinsic width. Actions within their group can wrap; inline retains one outer row and stacked always places actions below the heading. SLeftSidebar.headerLayout delegates the same axis to SPanel, defaulting to auto; a compact title and action may use inline without consumer CSS.

Action/link chip всегда сохраняет видимый интерактивный контур, включая severity="secondary" на subtle-поверхности; static chip не получает ложного affordance. Для строк и карточек с дополнительными controls используйте SActionCard#actions или SActionListItem#actions. actions-layout/ trailing-layout задаёт inline, stacked или container-responsive композицию. Основное действие и trailing controls остаются sibling-элементами внутри одной визуальной поверхности, без вложенных интерактивных элементов и дублирования рамки.

Action and link chips always retain a visible interactive outline, including a secondary chip on a subtle surface; static chips do not gain a false action affordance. For rows and cards with additional controls, use SActionCard#actions or SActionListItem#actions. The actions-layout and trailing-layout props select inline, stacked, or container-responsive composition. The primary action and trailing controls remain siblings inside a single visual surface, with no nested interactive elements or duplicated chrome.

SActionListItem предоставляет постоянный #leading, краткий #metadata и actionsVisibility="interaction" для обмена metadata/actions в одной trailing-области. Keyboard, touch и недоступный primary сохраняют прямой доступ к действиям. tooltipTrigger у List/Card передаётся canonical activation owner. trailingWidth="stable" — default: exchange-область резервирует ширину обоих наборов без суммирования. trailingWidth="current" при interaction учитывает только текущие metadata или actions в normal flow; скрытый набор не резервирует место. Смена policy сохраняет mounted controls, focus, draft и overlay lifetime; вложенные строки задают policy независимо. Тип ActionListItemTrailingWidth экспортируется из shared/data-display/SActionListItem.vue. titleOverflow="wrap" показывает полный буквальный title, включая пробелы и переводы строк, в доступной ширине; default ellipsis сохраняет однострочное усечение. Политика принадлежит собственному title, не вложенным строкам. Публичный тип ActionListItemTitleOverflow экспортируется из .vue subpath. В stacked и узкой responsive-композиции статусы и действия переносятся по ширине карточки; полные подписи отдельных действий сохраняют собственную строку. API, размеры, RTL и пример композиции: action-list-item.md. #body="{ disabled }" у List/Card размещает полноширинный редактор под primary/actions без вложения в activation. Высота определяется содержимым; consumer владеет раскрытием и явно передаёт disabled полям. С body fill означает минимальное заполнение, а внешний viewport владеет прокруткой. SActionList.maxHeight="none" — default: список сохраняет естественную высоту и видимое переполнение, не создаёт собственный scroll container и не удерживает wheel-прокрутку над обычными строками внутри внешнего viewport. sm/md/lg ограничивают высоту, включают собственную прокрутку списка и удерживают её цепочку на границе.

SActionListItem provides persistent #leading, short #metadata and actionsVisibility="interaction" to exchange metadata/actions within one trailing region. Keyboard, touch and unavailable primary retain direct action access. List/Card tooltipTrigger forwards to the canonical activation owner. trailingWidth="stable" is the default: the exchange region reserves both regions' widths without adding them. With interaction, trailingWidth="current" uses only the current metadata or actions in normal flow; the hidden region reserves no space. Policy changes retain mounted controls, focus, drafts and overlay lifetime; nested rows choose their own policy. The public ActionListItemTrailingWidth type is exported from shared/data-display/SActionListItem.vue. titleOverflow="wrap" displays the complete literal title, preserving spaces and line breaks within the available width; default ellipsis keeps single-line truncation. The policy belongs to the own title, not nested rows. The public ActionListItemTitleOverflow type is exported from the .vue subpath. Stacked and narrow responsive compositions wrap status regions and actions to the card width while preserving complete ordinary action labels. API, sizing, RTL and composition example: action-list-item.md. List/Card #body="{ disabled }" places a full-width editor below primary/actions, outside activation. Height is content-driven; consumers own expansion and bind disabled to their fields. With body, fill is minimum-fill and an outer viewport owns scrolling. SActionList.maxHeight="none" is the default: the list keeps its natural height and visible overflow, does not create its own scroll container, and lets wheel input over ordinary rows reach the outer viewport. sm/md/lg cap height, enable the list's own scrolling, and contain its scroll chain at the boundary.

Лёгкий и редактируемый код / Lightweight and editable code

SCodeBlock загружает CodeMirror отдельным async chunk только для presentation="editor". Для read-only текста, логов и конфигурации используйте presentation="plain": header, copy action, accessible name, line numbers и semantic viewport сохраняются, а editor/language runtime не загружается.

SCodeBlock loads CodeMirror in a separate async chunk only for presentation="editor". Use presentation="plain" for read-only text, logs, and configuration: the header, copy action, accessible name, line numbers, and semantic viewport remain available without loading the editor/language runtime.

Read-only plain и highlighted используют именованную group на native pre: требуется ровно один ariaLabel/ariaLabelledby, включая пустой вывод. Текст остаётся доступным внутри группы; номера строк скрыты от screen reader. code.textContent сохраняет буквальный source, включая пустой текст и завершающие newline. Номера строк расположены вне code; высота пустых строк задаётся layout без текстовых struts. DOM Range и кнопка Copy возвращают исходник точно. Нативное выделение и keyboard Copy следуют rendered-text правилам браузера для pre; номера или синтетические символы исключены. focus() и Tab фокусируют саму поверхность. PageDown/Home/End прокручивают её нативно при переполнении выбранного viewport, без дополнительных tab stops. Scroll-контейнер удерживает boundary-прокрутку внутри себя; Tab выходит к следующему control. Отрисовка текста после раскрытия и возврата в viewport принадлежит SCodeBlock; потребителю не нужны чтение геометрии, принудительный focus или CSS-переопределения.

Read-only plain and highlighted expose a named group on the native pre. Exactly one ariaLabel/ariaLabelledby is required, including empty output. Text remains readable within the group; line numbers are hidden from screen readers. focus() and Tab focus the surface itself. PageDown/Home/End use native scrolling when its viewport overflows, without additional tab stops. The scroll container contains boundary scrolling; Tab exits to the next control. code.textContent retains the literal source, including empty text and trailing newlines. Line numbers live outside code; empty-row height is layout-owned, without text struts. DOM Range and the Copy action return the exact source. Native selection and keyboard Copy follow the browser's rendered-text rules for pre, excluding line numbers and synthetic characters. SCodeBlock owns text paint after disclosure and viewport return; consumers do not need geometry reads, forced focus, or CSS overrides.

<SCodeBlock
  :code="diagnostic"
  :language="{ mode: 'explicit', id: 'text' }"
  presentation="plain"
  aria-label="Диагностика подключения"
/>

Plain presentation допускает только readOnly=true и явно отклоняет editor-only interceptKeys, nextSelectionPos и applySelection.

Plain presentation accepts readOnly=true only and explicitly rejects the editor-only interceptKeys, nextSelectionPos, and applySelection contracts.

headerActions и явный headerActionContext добавляют consumer-owned действия к шапке SCodeBlock; headerActionsPresentation="menu" использует общее меню. header-action сохраняет точные code/language и source identity. Для SRichDocument используются sparse codeActions и codeActionSource принятого snapshot; code-action адресует конкретный fence. Типы и occurrence helper экспортируются из shared/data-display/codeActions. Выполнение остаётся у приложения. Контракт: rich-document.md.

SCodeBlock header actions use an explicit source context and inline/menu presentation; SRichDocument binds actions to exact accepted-snapshot code occurrences through its sidecar API. Typed intents retain literal code/language and source identity. Execution remains application-owned.

SCodeEditor сохраняет одну native view/history при изменении theme, language, capabilities и controlled source. Mixed LF/CRLF/CR и literal UTF-16 offsets сохраняются в undo/redo, включая разные длины и separator-only replacements. Контракт: code-editor.md.

SCodeEditor retains one native view/history across configuration and controlled source updates; undo/redo preserves mixed literal separators and UTF-16 coordinates, including length and separator-only replacements.

Принятые матрицы и команды редактора / Accepted matrices and editor commands

SDataGrid.mode="matrix" показывает принятую readonly страницу буквальных строк, используя общих table/scroll owners и bounded виртуализацию обеих осей. Host сохраняет source, paging и чтение файлов; props реестра и редактирования не смешиваются с matrix API. Полное значение active cell доступно через SKeyValueGrid.valueWhitespace="preserve". Контракт и пример: data-grid.md.

SDataGrid.mode="matrix" presents an accepted read-only literal-string page through shared table/scroll owners and bounded two-axis virtualization. Source, paging and file reads remain host-owned; records/editing props cannot mix with matrix APIs. The active value uses SKeyValueGrid.valueWhitespace="preserve". See the data-grid contract.

Detect-язык редактора допускает unknownLanguage="plain-text" для неизвестных текстовых файлов, сохраняя parsers поддерживаемых языков. capabilities.keyInterceptorPolicy="always" разрешает host-команды в focused locked editor без разрешения ввода. / Editor detection accepts explicit unknownLanguage="plain-text" for unknown text files while retaining supported parsers; capabilities.keyInterceptorPolicy="always" permits host commands in a focused locked editor without unlocking input.

Пассивный текст и подписи ячеек / Passive text and cell labels

SText принимает точный text, включая пустую строку и пробелы. typography="body" | "title" | "label" | "meta" задаёт визуальную роль; default body не создаёт heading semantics. overflow="wrap" сохраняет переносы и полное содержимое, ellipsis ограничивает представление одной строкой, сохраняя исходный текст в DOM. monospace меняет только семейство шрифта и по умолчанию выключен. Пассивный span не создаёт роль или Tab stop. Body/title/label используют 14/24 px, meta — 12/20 px на root 16 px; типографические роли сохраняются при monospace.

Ячейка с названием и путём использует SStack gap="none" fill="container", SText с typography="label" и typography="meta" overflow="ellipsis". Для полного пути при hover и keyboard focus композиция использует STooltip и STooltipAnchor width="full" с одним доступным именем. Стек занимает доступную ширину anchor. STable сохраняет собственный focus owner строки; SText не добавляет остановок табуляции. Внутри интерактивного предка используется split-контракт focusOwner / pointerTarget: focus принадлежит предку, а SText.getElement() связывает пассивную цель. Обёртка STooltipAnchor внутри кнопки не применяется.

SText accepts exact text, including an empty string and whitespace. typography="body" | "title" | "label" | "meta" selects visual typography; the default body does not create heading semantics. The default overflow="wrap" preserves line breaks and full content; ellipsis presents one bounded line while retaining the original DOM text. monospace changes only the font family and defaults to false. The passive span adds neither a role nor a Tab stop. Body/title/label use 14/24 px and meta uses 12/20 px at a 16 px root; monospace retains the selected typography metrics.

Compose a name/path cell with SStack gap="none" fill="container", a label SText, and a meta SText overflow="ellipsis". STooltip with a full-width STooltipAnchor provides one named focus stop for the complete path. The stack explicitly fills the anchor's available width. STable retains its row focus owner; SText adds no tab stops. Inside an interactive ancestor, use the existing split focusOwner / pointerTarget contract and SText.getElement() for the passive target; the ancestor retains focus ownership, without a nested tooltip anchor.

Пустое рабочее пространство / Empty workspace

SEmptyState presentation="hero" размещает декоративную иконку, заголовок, описание и действия вертикально по центру. Компонент занимает доступную ширину без рамки, фона, margin, padding или минимальной высоты. Заголовок и описание полностью переносятся внутри ограниченной ширины, включая длинные слова. size="md" использует шкалу иконка/заголовок/описание 48/18/14 px, size="sm" — 32/14/12 px при root 16 px. Иконка и описание необязательны. Default presentation="panel", size="md" и режим inline сохраняют собственные layout-контракты.

Ровно один источник заголовка обязателен: непустой title или пассивный #title. Optional headingLevel принимает целое число от 1 до 6 и создаёт соответствующий native heading для title prop; omission оставляет div. Уровень выбирается по иерархии документа. Сочетание headingLevel и #title отклоняется: произвольный passive slot не гарантирует phrasing content заголовка. Действия передаются через существующий #actions; status root, заголовок и декоративная иконка не запрашивают focus или дополнительную Tab stop.

Высота рабочей области и вертикальное размещение принадлежат layout родителя. SStack fill="container" justify="center" align="stretch" центрирует hero в области с заданной родителем конечной доступной высотой. fill="container" не создаёт высоту viewport, а SEmptyState не резервирует искусственный вертикальный минимум. Размеры и типографика дочернего компонента не переопределяются consumer CSS.

SEmptyState presentation="hero" centers a vertical stack of decorative icon, title, description, and actions. It occupies the available width without a border, background, margin, padding, or minimum height. The title and description wrap completely within a bounded width, including long unbroken words. size="md" uses an icon/title/description scale of 48/18/14 px, while size="sm" uses 32/14/12 px at a 16 px root. The icon and description are optional. The default presentation="panel", size="md", and the inline presentation retain their own layout contracts.

Exactly one title source is required: a nonempty title prop or the passive #title slot. Optional headingLevel accepts an integer from 1 to 6 and renders the corresponding native heading for the title prop; omission retains a div. Choose the level according to the document hierarchy. Combining headingLevel with #title is rejected because arbitrary passive slot content is not guaranteed to satisfy a heading's phrasing-content model. Actions use the existing #actions slot; the status root, title, and decorative icon request neither focus nor an extra Tab stop.

Workspace height and vertical placement belong to the parent layout. SStack fill="container" justify="center" align="stretch" centers the hero within the finite available height supplied by its parent. fill="container" does not create a viewport height, and SEmptyState reserves no artificial vertical minimum. Consumer CSS does not override the child component's dimensions or typography.

<script setup lang="ts">
import { Chat } from '@pgcorp/ui-kit/icons'
import SStack from '@pgcorp/ui-kit/layout/SStack.vue'
import SEmptyState from '@pgcorp/ui-kit/shared/data-display/SEmptyState.vue'
</script>

<template>
  <SStack fill="container" gap="none" justify="center" align="stretch">
    <SEmptyState
      presentation="hero"
      size="md"
      :icon="Chat"
      :heading-level="2"
      title="Начните новый разговор"
      description="Выберите модель и отправьте первое сообщение."
    />
  </SStack>
</template>

Раскладка списка свойств / Property-list layout

SKeyValueGrid.itemLayout="row" сохраняет подпись и значение в двух колонках в узком inspector; stacked задаёт вертикальную пару, responsive сохраняет variant/container policy. Раскладка, относительная ширина подписи и whitespace управляются публичными props. / SKeyValueGrid.itemLayout="row" keeps a two-column pair in narrow inspectors; stacked is vertical and responsive follows the variant/container policy. See key-value-grid.md.

Иконки и размер сборки / Icons and bundle size

Для известной при сборке иконки используйте именованный export. Каталог публикует tree-shakeable компонент для каждого canonical icon ID, поэтому consumer bundle включает только импортированные иконки. Динамический getSputnigUiIconComponent предназначен для runtime-значений из каталога и по контракту включает полный registry.

Именованные exports и getter синхронны и сохраняют identity компонента. Набор classic отображается синхронно и не загружает custom-геометрию. iconSetMode="sputnig" загружает SVG-ресурс только выбранного canonical ID; одновременные владельцы разделяют запрос и immutable cache геометрии. Ожидание и отказ имеют собственные видимые состояния в том же квадрате, без подмены выбранного набора. Событие load-state предоставляет { name, iconSetMode, state }, при state="error" — также error: Error. Изменение неотрицательного целого retryKey после отказа запускает повторный сетевой запрос. Изменение ключа во время загрузки не создаёт второй запрос. Смена ID/набора и unmount исключают устаревшие рисунки и события.

Браузер получает package-relative SVG через fetch (connect-src 'self') и проверяет пассивную canonical SVG-форму перед inline-отображением. ES library и повторная consumer-сборка сохраняют отдельный SVG asset; его содержимое не встраивается в classic startup. SSR загружает body только выбранного ID и отдаёт готовую геометрию. Hydration удерживает её до завершения клиентской загрузки; иной сетевой результат заменяет только принадлежащий иконке body и сообщает actual state. Cache не хранит пользовательские labels, handlers, owner refs или ошибки SSR-запроса. Server и client используют артефакты одной сборки: publish-contract проверяет точное соответствие SSR body, SVG asset и canonical source каждого ID. Внутренняя граница package conditions отделяет Node SSR loaders от browser и default. В полном браузерном JS-графе отсутствуют SSR-каталог и inline custom bodies; выбранная SVG-геометрия остаётся отдельным ресурсом. Node SSR использует штатное разрешение пакета без consumer aliases или private imports. Автоматический SSR-контракт рассчитан на поддерживаемый Node runtime; edge/webworker SSR не входит в этот контракт. aria-labelledby самостоятельно задаёт доступное имя; декоративная иконка остаётся скрытой. Icon owner не добавляет tab-stop.

Иконки ленивого экрана остаются за его dynamic import. Динамические SputnigUiIcon и getter включают classic registry и небольшие descriptors, не все custom bodies. Бюджет старта учитывает транзитивный static/preload граф; host chunk groups должны сохранять эти границы. Число выпущенных JS assets и их полный import graph проверяются отдельно от startup/preload бюджета.

Use a named export for an icon known at build time. The catalog publishes a tree-shakeable component for every canonical icon ID, so the consumer bundle contains only imported icons. The dynamic getSputnigUiIconComponent API is reserved for runtime catalog values and intentionally includes the complete registry.

Named exports and the getter are synchronous and identity-stable. classic renders synchronously without custom geometry loading. iconSetMode="sputnig" fetches only the selected canonical SVG asset; live owners share its request and immutable geometry cache. Loading/error render explicit status bodies in the same square, not a classic fallback. load-state emits { name, iconSetMode, state }, with an owner-local error: Error on failure. Change the nonnegative integer retryKey after an error for a real network retry; changing it while loading does not duplicate the request. ID/set changes and unmount retire obsolete results and events.

Browser delivery uses package-relative fetch (connect-src 'self') and a strict passive SVG validator before inline rendering. Library and consumer builds retain external SVG assets, outside the classic startup payload. SSR loads only the selected ID and renders complete geometry. Hydration retains the server body while loading; a different network outcome replaces only the owned body and reports its actual state. The cache contains no labels, handlers, owner references, or request errors. Server and client use matching build artifacts; the publish contract verifies each ID's SSR body and SVG asset against its exact canonical source. An internal package-condition boundary separates Node SSR loaders from the browser and default conditions. The complete browser JavaScript graph contains neither the SSR catalog nor inline custom bodies; selected SVG geometry remains an external resource. Node SSR uses native package resolution without consumer aliases or private imports. Automatic SSR delivery targets the supported Node runtime; edge/webworker SSR is outside this contract. aria-labelledby alone names the image; decorative icons remain hidden and create no extra tab stop. Dynamic APIs include the classic registry and small descriptors, not eager custom bodies. Preserve feature boundaries in host chunk groups and measure the transitive static-import/preload closure, not a single entry file. Audit all emitted JavaScript assets and their complete import graph separately from the startup/preload budget.

import {
  ArrowUpRight, Bell, Cable, CalendarDays, Disc, GitCompare, GitFork,
  ChevronLeft, HardDrive, PanelLeftOpen, Paperclip, Pause, Radio, RotateCw, Usb, Volume2, Zap, ZoomIn, ZoomOut,
} from '@pgcorp/ui-kit/icons'

HardDrive, Usb и Disc обозначают локальный накопитель, USB-подключение и оптический диск; CalendarDays — дату, ArrowUpRight — переход или открытие. Paperclip обозначает прикрепление файла, GitFork — разветвление разговора; ZoomIn/ZoomOut — изменение масштаба изображения, RotateCw — поворот по часовой стрелке, Volume2 — аудио. Поворот против часовой стрелки представлен отдельным RotateCcw, а не alias для RotateCw. Radio обозначает сетевое вещание или фоновое уведомление, не radio input. Pause обозначает состояние паузы задачи; ChevronLeft — переход влево. Zap обозначает молнию и подходит для роли быстрой модели. Это primary-only силуэт: duotone сохраняет одну цветовую роль, без дополнительного accent. Все используют общий icon contract: iconSetMode (classic, sputnig), colorMode (auto, duotone, monochrome), тему и tree-shakeable ESM exports. Внутри control иконка передаётся через его icon slot; размер и состояние задаёт control owner.

HardDrive, Usb, and Disc represent a local drive, a USB connection, and an optical disc; CalendarDays represents a date, and ArrowUpRight represents navigation or opening. All share the icon contract: iconSetMode (classic, sputnig), colorMode (auto, duotone, monochrome), theme support, and tree-shakeable ESM exports. Pass an icon through the control's declared icon prop or slot; the control owner defines its size and state. SButton accepts leadingIcon and trailingIcon.

Paperclip represents file attachment and GitFork conversation branching. ZoomIn/ZoomOut represent image zoom, RotateCw clockwise rotation, and Volume2 audio. Counterclockwise rotation has a distinct RotateCcw export; the two rotation actions are not aliases. Radio represents network broadcast or a background notification, not a radio input. Pause represents a paused task; ChevronLeft represents leftward navigation. Zap represents a lightning bolt and can identify the fast-model role. Its primary-only silhouette retains one color role in duotone, without an accent shape.

<SButton label="Выбрать локальный накопитель" :leading-icon="HardDrive" />

Скачивание через ссылку / Native link downloads

SButton и SLink поддерживают typed download?: string | boolean только в href-режиме. Filename передаётся native anchor буквально; true/"" оставляют выбор имени браузеру, false не создаёт download attribute. Router to и action button не принимают этот prop. Consumer владеет полным Blob и object URL; ограниченный preview не становится источником скачивания. SLink.disabled и SButton.disabled/loading блокируют activation и удаляют href/download.

SButton and SLink support typed download?: string | boolean in href mode only. The filename is passed verbatim to the native anchor; true/"" let the browser choose it, and false omits the attribute. Router to and action buttons reject this prop. The consumer owns the complete Blob and its object URL; a limited preview is not the download source. SLink.disabled and SButton.disabled/loading block activation and remove href/download.

Контракт, Blob lifecycle и ограничения браузера включены в пакет: docs/link-download.md. The package includes the contract, Blob lifecycle, and browser limitations in docs/link-download.md.

Идентичность динамических вкладок / Dynamic tab identity

В managed-композиции STabs значение modelValue, STab.name и STabPanel.name используют один стабильный ID сущности. Vue key также основан на этом ID. Добавление элемента и выбор его ID допускаются в одной reactive update, включая вставку в начало массива. Порядковый номер и подпись вкладки являются отображением, а не идентичностью.

Для E2E consumer задаёт собственные data-* маркеры по ID и проверяет роль tab, aria-selected, связь aria-controls с ID панели и обратную aria-labelledby. Внутренний формат DOM ID, nth() и индекс в подписи не являются selector-контрактом. Сохранённые панели сохраняют свой input при переключении; ручная DOM-активация и remount для выбора не требуются.

In managed STabs, modelValue, STab.name, STabPanel.name, and Vue key use the same stable entity ID. Adding a keyed item and selecting its ID can occur in one reactive update, including insertion at the beginning. An ordinal or label is presentation, not identity. E2E tests use consumer-owned data-* IDs, the tab role, aria-selected, and the matching aria-controls / aria-labelledby relationship. Internal DOM ID formats, nth(), and label indices are not selector contracts. Retained panels preserve their input; selection does not require manual DOM activation or remounting.

Горизонтальный STabList при изменении selection или геометрии раскрывает зарегистрированного участника с текущим фокусом; без такого фокуса — выбранную вкладку. Если целочисленная scroll-позиция позволяет полностью разместить обоих участников, выбранная вкладка также остаётся видимой; иначе приоритет имеет участник с фокусом. Расчёт использует реальное окно прокрутки, включая RTL. Обычное изменение раскладки раскрывает ближайшую границу и не центрирует уже видимую вкладку. Поправка видимости округляется в сторону раскрываемой границы, чтобы целочисленная прокрутка браузера не оставляла дробный срез вкладки за окном. Сама ручная прокрутка и обновления без изменения геометрии не возвращают scroll position назад. Фокус, selection и retained panels не меняются. scrollActiveIntoCenter() явно центрирует выбранную вкладку в штатном диапазоне прокрутки браузера; обычный controlled выбор не требует вызова этой команды.

A horizontal STabList reveals the currently focused registered participant after selection or geometry changes, or the selected tab when no participant owns focus. When an integer scroll position can fully contain both participants, the selected tab remains visible as well; otherwise, the focused participant takes priority. Calculations use the native scroll viewport, including RTL. Ordinary layout changes reveal the nearest edge without recentering an already visible tab. Visibility repairs round toward the revealed edge so integer browser scrolling does not leave a fractional slice outside the viewport. Manual scrolling and updates without geometry changes do not reset the scroll position. Focus, selection, and retained panels are unchanged. scrollActiveIntoCenter() explicitly centers the selected tab within the browser's native scroll range; ordinary controlled selection does not require this command.

STabs.surfaceBorder="default" | "none" управляет только внешним периметром, независимо от variant, surfaceRadius и surface. Default default сохраняет оформление выбранного variant, включая отсутствие рамки у flat. none убирает все четыре внешние стороны; разделитель STabList и selected indicator STab сохраняются. Icon-only/equal геометрия, keyboard, selection, focus и retained panels не меняются. Для панели с собственной границей используйте surface-border="none", не заменяя variant="icon-only" на flat. Тип STabsSurfaceBorder экспортируется из @pgcorp/ui-kit/shared/navigation/STabs.vue; неизвестные значения отклоняются.

STabs.surfaceBorder="default" | "none" controls only the outer perimeter, independently of variant, surfaceRadius, and surface. The default value preserves the selected variant's presentation, including the borderless flat variant. none removes all four outer edges; the STabList divider and STab selected indicator remain. Icon-only/equal geometry, keyboard behavior, selection, focus, and retained panels are unchanged. A host panel with its own boundary uses surface-border="none" without replacing variant="icon-only" with flat. The public STabsSurfaceBorder type is exported from @pgcorp/ui-kit/shared/navigation/STabs.vue; unknown values are rejected.

Consumer-owned data-testid сохраняется на root STabs при отсутствии testId. Явный prop имеет приоритет при обоих каналах; его удаление восстанавливает consumer-owned marker без замены root или изменения других data-*.

STabs retains a consumer-owned data-testid on its root when testId is absent. An explicit prop takes precedence when both channels are supplied; withdrawing it restores the consumer-owned marker without replacing the root or changing other data-* attributes.

STabs variant="bottom" размещает list после panels и фиксирует owner у нижней границы viewport. Для embedded viewport host задаёт собственный bounded контейнер с contain: layout; стили внутренностей STabs не переопределяются. Обычная локальная нижняя строка задаётся custom-композицией STabList placement="bottom" без viewport-варианта owner.

STabs variant="bottom" places the list after the panels and fixes the owner to the viewport bottom. An embedded viewport uses a bounded host container with contain: layout, without overriding STabs internals. A local bottom row uses custom composition with STabList placement="bottom", without the owner viewport variant.

Overlay-действия входят в минимальную ширину своей вкладки даже при короткой метке; центр trigger сохраняет самостоятельную область