@pgcorp/ui-kit
v0.19.0
Published
Typed Vue 3 design system with accessible components, semantic themes, and workbench patterns.
Maintainers
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.28postcss.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 сохраняет самостоятельную область
