@pgcorp/ui-kit 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +188 -9
  2. package/docs/getting-started.md +135 -0
  3. package/docs/public-api.md +53 -4
  4. package/docs/rich-document.md +216 -0
  5. package/docs/scroll-viewport.md +167 -0
  6. package/package.json +103 -50
  7. package/src/components/layout/SAppShell.vue +3 -1
  8. package/src/components/layout/SDockRegion.vue +5 -3
  9. package/src/components/layout/SWorkbenchPanelShell.vue +4 -2
  10. package/src/components/shared/_internal/SDialogSurface.css +5 -0
  11. package/src/components/shared/_internal/SDialogSurface.vue +3 -1
  12. package/src/components/shared/_internal/SToastItem.css +4 -0
  13. package/src/components/shared/_internal/SToastItem.vue +44 -8
  14. package/src/components/shared/_internal/STreeNode.vue +24 -11
  15. package/src/components/shared/complex/SKanbanLane.vue +4 -2
  16. package/src/components/shared/containers/SCloudChromeRow.vue +3 -2
  17. package/src/components/shared/containers/SDrawer.vue +5 -2
  18. package/src/components/shared/containers/SModal.vue +5 -2
  19. package/src/components/shared/containers/SPanel.vue +4 -2
  20. package/src/components/shared/containers/SPopover.vue +7 -3
  21. package/src/components/shared/containers/SUiKitLocaleProvider.vue +57 -0
  22. package/src/components/shared/controls/SButton.css +14 -10
  23. package/src/components/shared/controls/SCombobox.vue +8 -6
  24. package/src/components/shared/controls/SComboboxTrigger.css +7 -4
  25. package/src/components/shared/controls/SComboboxTrigger.vue +5 -2
  26. package/src/components/shared/controls/SContextToggleButton.vue +6 -4
  27. package/src/components/shared/controls/SDragHandle.vue +8 -4
  28. package/src/components/shared/controls/SDropdownMenu.vue +7 -4
  29. package/src/components/shared/controls/SInputText.css +5 -4
  30. package/src/components/shared/controls/SListbox.vue +4 -2
  31. package/src/components/shared/controls/SSelect.vue +4 -2
  32. package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +4 -2
  33. package/src/components/shared/data-display/SChart.vue +7 -5
  34. package/src/components/shared/data-display/SChip.vue +3 -1
  35. package/src/components/shared/data-display/SCodeBlock.css +4 -0
  36. package/src/components/shared/data-display/SCodeBlock.vue +42 -20
  37. package/src/components/shared/data-display/SCodeEditor.css +17 -0
  38. package/src/components/shared/data-display/SCodeEditor.vue +173 -8
  39. package/src/components/shared/data-display/SCodeSearchPanel.vue +48 -33
  40. package/src/components/shared/data-display/SCodeWorkspaceEditor.css +13 -0
  41. package/src/components/shared/data-display/SCodeWorkspaceEditor.vue +355 -0
  42. package/src/components/shared/data-display/SControlledToastViewport.css +89 -0
  43. package/src/components/shared/data-display/SControlledToastViewport.vue +205 -0
  44. package/src/components/shared/data-display/SCopyField.vue +12 -10
  45. package/src/components/shared/data-display/SDiffViewer.css +122 -0
  46. package/src/components/shared/data-display/SDiffViewer.vue +499 -0
  47. package/src/components/shared/data-display/SDocBlock.vue +99 -71
  48. package/src/components/shared/data-display/SExpandableText.vue +10 -8
  49. package/src/components/shared/data-display/SJsonTree.vue +4 -2
  50. package/src/components/shared/data-display/SLinkedSystemsList.vue +4 -2
  51. package/src/components/shared/data-display/SMessage.vue +6 -6
  52. package/src/components/shared/data-display/SRichDocument.css +8 -0
  53. package/src/components/shared/data-display/SRichDocument.vue +65 -0
  54. package/src/components/shared/data-display/SScrollViewport.css +53 -0
  55. package/src/components/shared/data-display/SScrollViewport.vue +470 -0
  56. package/src/components/shared/data-display/STable.vue +38 -19
  57. package/src/components/shared/data-display/SToastContainer.vue +22 -17
  58. package/src/components/shared/data-display/_internal/SCodeTokenLine.css +72 -0
  59. package/src/components/shared/data-display/_internal/SCodeTokenLine.vue +79 -0
  60. package/src/components/shared/data-display/_internal/SRichBlockNode.css +85 -0
  61. package/src/components/shared/data-display/_internal/SRichBlockNode.vue +282 -0
  62. package/src/components/shared/data-display/_internal/SRichInlineNode.css +6 -0
  63. package/src/components/shared/data-display/_internal/SRichInlineNode.vue +76 -0
  64. package/src/components/shared/data-display/_internal/SRichMath.css +42 -0
  65. package/src/components/shared/data-display/_internal/SRichMath.vue +102 -0
  66. package/src/components/shared/data-display/codeEditor.ts +200 -0
  67. package/src/components/shared/data-display/codeWorkspace.ts +233 -0
  68. package/src/components/shared/data-display/diff.ts +541 -0
  69. package/src/components/shared/data-display/richDocument.ts +876 -0
  70. package/src/components/shared/data-display/scrollViewport.ts +47 -0
  71. package/src/components/shared/data-display/toast.ts +194 -0
  72. package/src/components/shared/database/SDataGrid.vue +7 -4
  73. package/src/components/shared/database/SSqlEditor.vue +39 -39
  74. package/src/components/shared/feedback/SAsyncState.vue +3 -1
  75. package/src/components/shared/graph/SGraphViewport.vue +11 -9
  76. package/src/components/shared/navigation/SBreadcrumbs.vue +8 -5
  77. package/src/components/shared/navigation/SCatalogNavigator.vue +26 -20
  78. package/src/components/shared/navigation/STabList.vue +10 -6
  79. package/src/components/shared/navigation/STabs.vue +15 -11
  80. package/src/components/shared/navigation/SWizardSteps.vue +4 -2
  81. package/src/components/shared/persona/SPersonaProjectPicker.vue +31 -27
  82. package/src/components/shared/persona/SPersonaRunWorkbench.vue +39 -33
  83. package/src/components/shared/persona/_internal/SPersonaRunConsole.vue +8 -6
  84. package/src/components/shared/persona/_internal/labels.ts +66 -57
  85. package/src/components/shared/persona/presentation.ts +140 -110
  86. package/src/composables/useFloatingPosition.ts +39 -2
  87. package/src/composables/useUiKitLocale.ts +17 -0
  88. package/src/internal/codeHighlight.ts +110 -0
  89. package/src/internal/diffEngine.ts +464 -0
  90. package/src/internal/diffRuntime.ts +189 -0
  91. package/src/internal/feedbackPresentation.ts +33 -14
  92. package/src/internal/localeContext.ts +22 -0
  93. package/src/internal/ownedAttrs.ts +8 -0
  94. package/src/internal/runtimeContract.ts +11 -9
  95. package/src/internal/semanticSizing.ts +13 -0
  96. package/src/locales/catalog.ts +634 -0
  97. package/src/locales/locale.ts +472 -0
  98. package/src/locales/messages.generated.ts +1826 -0
  99. package/src/locales/messages.ts +150 -0
  100. package/src/stores/useNotifierStore.ts +61 -44
  101. package/src/styles/tokens.css +20 -2
  102. package/src/theme.ts +59 -32
  103. package/src/workers/diff.worker.ts +73 -0
  104. package/src/workers/diffProtocol.ts +155 -0
  105. package/src/components/shared/data-display/SToastContainer.css +0 -53
@@ -0,0 +1,216 @@
1
+ # Безопасные rich documents / Safe rich documents
2
+
3
+ ## Назначение / Purpose
4
+
5
+ `SRichDocument` отображает сериализуемый документ по закрытому контракту
6
+ `SRichDocumentV1`. Компонент предназначен для содержимого, которое приходит из
7
+ worker, backend или persisted state и должно содержать типографику, ссылки,
8
+ таблицы, код, формулы, изображения, файлы и типизированные действия.
9
+
10
+ `SRichDocument` renders a serializable document described by the closed
11
+ `SRichDocumentV1` contract. It is intended for content received from a worker,
12
+ backend, or persisted state that needs typography, links, tables, code, math,
13
+ images, files, and typed actions.
14
+
15
+ Markdown и HTML strings относятся к контракту `SDocBlock`. `SRichDocument` не
16
+ принимает raw HTML, VNodes, callbacks, component names или trust flags.
17
+
18
+ Markdown and HTML strings belong to the `SDocBlock` contract. `SRichDocument`
19
+ does not accept raw HTML, VNodes, callbacks, component names, or trust flags.
20
+
21
+ ## Импорты / Imports
22
+
23
+ ```ts
24
+ import SRichDocument from '@pgcorp/ui-kit/shared/data-display/SRichDocument.vue'
25
+ import {
26
+ SRichDocumentContractError,
27
+ validateRichDocument,
28
+ validateRichDocumentResources,
29
+ type SRichDocumentActionIntent,
30
+ type SRichDocumentResource,
31
+ type SRichDocumentResourceActivate,
32
+ type SRichDocumentV1,
33
+ } from '@pgcorp/ui-kit/shared/data-display/richDocument'
34
+ ```
35
+
36
+ ## Граница данных / Data boundary
37
+
38
+ Внешний JSON проверяется в месте получения. Валидатор возвращает новый
39
+ типизированный результат только после проверки версии, структуры, полей,
40
+ лимитов, идентификаторов, ссылок на resources и URL policy. Renderer повторяет
41
+ валидацию перед созданием DOM; это защищает прямое использование компонента, но
42
+ не заменяет проверку на application boundary.
43
+
44
+ Validate external JSON at the receiving boundary. The validator returns a
45
+ typed result only after checking the version, structure, fields, limits, IDs,
46
+ resource references, and URL policy. The renderer validates again before
47
+ creating DOM; this protects direct component use but does not replace validation
48
+ at the application boundary.
49
+
50
+ ```ts
51
+ const document = validateRichDocument(workerMessage.document)
52
+ const resources = validateRichDocumentResources(apiResponse.resources)
53
+ ```
54
+
55
+ `SRichDocumentContractError` содержит стабильный `code`, точный `path` и
56
+ диагностическое сообщение. Не исправляйте payload downstream и не отбрасывайте
57
+ неизвестные nodes: ошибка означает несовместимый или опасный вход.
58
+
59
+ `SRichDocumentContractError` exposes a stable `code`, an exact `path`, and a
60
+ diagnostic message. Do not repair the payload downstream or drop unknown nodes:
61
+ the error represents incompatible or unsafe input.
62
+
63
+ ```ts
64
+ try {
65
+ const document = validateRichDocument(payload)
66
+ useDocument(document)
67
+ } catch (error) {
68
+ if (error instanceof SRichDocumentContractError) {
69
+ reportContractFailure(error.code, error.path)
70
+ }
71
+ throw error
72
+ }
73
+ ```
74
+
75
+ Links принимают относительные application paths и `http:`, `https:`, `mailto:`,
76
+ `tel:`. Resource records принимают относительные paths, `http:`, `https:` и
77
+ `blob:`. Скриптовые, data, scheme-relative и неизвестные protocols отклоняются.
78
+ Resources передаются отдельными consumer-owned records и связываются с AST по
79
+ stable ID; UI-kit не читает filesystem и не выполняет URL самостоятельно.
80
+
81
+ Links accept relative application paths plus `http:`, `https:`, `mailto:`, and
82
+ `tel:`. Resource records accept relative paths, `http:`, `https:`, and `blob:`.
83
+ Script, data, scheme-relative, and unknown protocols are rejected. Resources
84
+ are supplied as separate consumer-owned records and joined to the AST by stable
85
+ ID; the UI kit does not read the filesystem or activate URLs itself.
86
+
87
+ ## Модель документа / Document model
88
+
89
+ `SRichDocumentV1` содержит `version: 1`, стабильный `id` и readonly `nodes`.
90
+ Закрытая модель поддерживает:
91
+
92
+ `SRichDocumentV1` contains `version: 1`, a stable `id`, and readonly `nodes`.
93
+ The closed model supports:
94
+
95
+ - paragraph, heading, strong, emphasis, inline code и links с optional
96
+ `current`-состоянием;
97
+ - ordered/unordered lists, quote и divider;
98
+ - code blocks с optional language, line numbers и label;
99
+ - inline/block math;
100
+ - semantic tables и callouts;
101
+ - image/file resources по ID;
102
+ - action groups с serializable action IDs.
103
+
104
+ ```ts
105
+ const document = {
106
+ version: 1,
107
+ id: 'release-notes',
108
+ nodes: [
109
+ {
110
+ type: 'heading',
111
+ level: 1,
112
+ children: [{ type: 'text', value: 'Release notes' }],
113
+ },
114
+ {
115
+ type: 'paragraph',
116
+ children: [{
117
+ type: 'link',
118
+ href: '/releases/current',
119
+ current: true,
120
+ children: [{ type: 'text', value: 'Current release' }],
121
+ }],
122
+ },
123
+ {
124
+ type: 'resource',
125
+ resourceId: 'report',
126
+ label: 'Open report',
127
+ },
128
+ {
129
+ type: 'actionGroup',
130
+ label: 'Review actions',
131
+ actions: [{ type: 'action', id: 'approve', label: 'Approve' }],
132
+ },
133
+ ],
134
+ } as const satisfies SRichDocumentV1
135
+
136
+ const resources = [{
137
+ id: 'report',
138
+ kind: 'file',
139
+ url: '/reports/release.pdf',
140
+ label: 'Release report',
141
+ mediaType: 'application/pdf',
142
+ }] as const satisfies readonly SRichDocumentResource[]
143
+ ```
144
+
145
+ ## Controlled intents
146
+
147
+ Ссылки, resources и actions не выполняют application effects. Компонент эмитит
148
+ `link-activate`, `resource-activate` и `action` с `documentId`, точным node
149
+ `path` и целевым ID/URL. Consumer проверяет разрешения, выполняет навигацию,
150
+ скачивание или доменное действие.
151
+
152
+ Links, resources, and actions never execute application effects. The component
153
+ emits `link-activate`, `resource-activate`, and `action` with the `documentId`,
154
+ exact node `path`, and target ID or URL. The consumer authorizes and performs
155
+ navigation, download, or domain actions.
156
+
157
+ `current: true` у link-node передаёт каноническому `SLink` состояние текущего
158
+ назначения и публикует `aria-current="page"`. Поле описывает состояние
159
+ навигации, но не запускает переход и не меняет typed intent.
160
+
161
+ `current: true` on a link node delegates the current-destination state to the
162
+ canonical `SLink` and publishes `aria-current="page"`. The field describes
163
+ navigation state without triggering navigation or changing the typed intent.
164
+
165
+ ```vue
166
+ <SRichDocument
167
+ :document="document"
168
+ :resources="resources"
169
+ accessible-label="Release notes"
170
+ @link-activate="openLink"
171
+ @resource-activate="openResource"
172
+ @action="runDocumentAction"
173
+ />
174
+ ```
175
+
176
+ `accessible-label` обязателен и задаёт имя root article. Consumer-owned
177
+ `data-*` разрешены как test/tour hooks; private classes и `data-s-*` не являются
178
+ selector API.
179
+
180
+ `accessible-label` is required and names the root article. Consumer-owned
181
+ `data-*` are supported as test and tour hooks; private classes and `data-s-*`
182
+ are not selector APIs.
183
+
184
+ ## Code, math, SSR и производительность
185
+
186
+ Подсветка кода и KaTeX загружаются отдельными lazy chunks. Code block использует
187
+ read-only token presentation, а не CodeMirror editor runtime. KaTeX работает с
188
+ `trust: false` и создаёт HTML+MathML внутри leaf renderer. Невалидная формула
189
+ показывает локализованную диагностируемую ошибку и не превращается в trusted
190
+ markup.
191
+
192
+ Syntax highlighting and KaTeX load as separate lazy chunks. Code blocks use a
193
+ read-only token presentation rather than the CodeMirror editor runtime. KaTeX
194
+ runs with `trust: false` and produces HTML+MathML inside a leaf renderer. An
195
+ invalid formula produces a localized, diagnosable error and never becomes
196
+ trusted markup.
197
+
198
+ SSR выводит стабильный безопасный fallback для code/math. Client runtime после
199
+ mount заменяет его подсвеченным кодом и формулой. Package не использует CDN и
200
+ не выполняет document parser в main-thread component.
201
+
202
+ SSR renders stable safe fallbacks for code and math. After mount, the client
203
+ runtime replaces them with highlighted code and rendered math. The package uses
204
+ no CDN and runs no document parser inside the main-thread component.
205
+
206
+ ## Версионирование / Versioning
207
+
208
+ `version` является частью persisted/transport contract. Не добавляйте
209
+ неизвестные поля и не подменяйте версию. Новая несовместимая схема получает
210
+ новую public version и явную migration policy; silent compatibility branches
211
+ не используются.
212
+
213
+ `version` is part of the persisted and transport contract. Do not add unknown
214
+ fields or rewrite the version. A new incompatible schema receives a new public
215
+ version and an explicit migration policy; silent compatibility branches are not
216
+ used.
@@ -0,0 +1,167 @@
1
+ # Область прокрутки / Scroll viewport
2
+
3
+ ## Назначение / Purpose
4
+
5
+ `SScrollViewport` является semantic owner прокручиваемой области. Компонент
6
+ задаёт доступное имя, разрешённые оси, размер, focus/keyboard contract,
7
+ overscroll policy и сериализуемое состояние границ. Consumer передаёт контент,
8
+ но не создаёт собственный `overflow: auto` container для этого паттерна.
9
+
10
+ `SScrollViewport` is the semantic owner of a scrollable region. It defines the
11
+ accessible name, enabled axes, size, focus and keyboard contract, overscroll
12
+ policy, and serializable boundary state. Consumers supply content instead of
13
+ creating an application-owned `overflow: auto` container for this pattern.
14
+
15
+ ## Импорты / Imports
16
+
17
+ ```ts
18
+ import SScrollViewport from '@pgcorp/ui-kit/shared/data-display/SScrollViewport.vue'
19
+ import type {
20
+ SScrollViewportApi,
21
+ SScrollViewportState,
22
+ } from '@pgcorp/ui-kit/shared/data-display/scrollViewport'
23
+ ```
24
+
25
+ ## Props
26
+
27
+ | Prop | Contract |
28
+ | --- | --- |
29
+ | `accessibleLabel` | Обязательное непустое имя `region`. / Required non-empty name of the `region`. |
30
+ | `axis` | `'x' \| 'y' \| 'both'`; default `y`. Запрещённая ось не прокручивается и недоступна API. / A disabled axis neither scrolls nor accepts API commands. |
31
+ | `blockSize` | `'sm' \| 'md' \| 'lg' \| 'fill'`; semantic token, raw CSS length запрещён. / Semantic token; raw CSS lengths are rejected. |
32
+ | `focusable` | Включает root tab stop и `focus()`; default `true`. / Enables the root tab stop and `focus()`; defaults to `true`. |
33
+ | `keyboardNavigation` | Управление корнем с клавиатуры; требует `focusable=true`. / Root keyboard control; requires `focusable=true`. |
34
+ | `overscrollBehavior` | `'auto' \| 'contain' \| 'none'`; default `contain`. |
35
+
36
+ Consumer-owned `data-*` разрешены на root region для tour/test hooks. Internal
37
+ classes и `data-s-*` не являются selector API.
38
+
39
+ Consumer-owned `data-*` are supported on the root region for tour and test
40
+ hooks. Internal classes and `data-s-*` are not selector APIs.
41
+
42
+ ## Состояние / State
43
+
44
+ `scroll-state` эмитится после mount, прокрутки, resize viewport/content и
45
+ изменения presentation props. Payload и вложенные записи immutable и
46
+ serializable:
47
+
48
+ `scroll-state` is emitted after mount, scrolling, viewport/content resize, and
49
+ presentation prop changes. The payload and nested records are immutable and
50
+ serializable:
51
+
52
+ ```ts
53
+ interface SScrollViewportState {
54
+ readonly physical: {
55
+ readonly x: number
56
+ readonly y: number
57
+ readonly maxX: number
58
+ readonly maxY: number
59
+ }
60
+ readonly semantic: {
61
+ readonly inline: number
62
+ readonly block: number
63
+ readonly maxInline: number
64
+ readonly maxBlock: number
65
+ }
66
+ readonly atInlineStart: boolean
67
+ readonly atInlineEnd: boolean
68
+ readonly atBlockStart: boolean
69
+ readonly atBlockEnd: boolean
70
+ }
71
+ ```
72
+
73
+ `physical.x` измеряется от физического левого края и нормализован между
74
+ browser RTL scroll models; `physical.y` — от верхнего края. Semantic offsets
75
+ учитывают `direction` и `writing-mode`. Boundary flags используют semantic
76
+ inline/block directions.
77
+
78
+ `physical.x` is measured from the physical left edge and normalized across
79
+ browser RTL scroll models; `physical.y` is measured from the top edge. Semantic
80
+ offsets follow `direction` and `writing-mode`. Boundary flags use semantic
81
+ inline and block directions.
82
+
83
+ ## Exposed API
84
+
85
+ Component ref реализует только `SScrollViewportApi`; DOM owner не экспортируется:
86
+
87
+ The component ref implements `SScrollViewportApi` only; its DOM owner is not
88
+ exposed:
89
+
90
+ ```ts
91
+ interface SScrollViewportApi {
92
+ focus(): void
93
+ scrollToBoundary(axis: 'x' | 'y', boundary: 'start' | 'end'): void
94
+ reveal(options: { x?: number; y?: number; behavior?: 'auto' | 'smooth' }): void
95
+ }
96
+ ```
97
+
98
+ `scrollToBoundary` и `reveal` проверяют разрешённые оси. Координаты `reveal`
99
+ задаются в physical left/top model и ограничиваются фактическим scroll range.
100
+ Вызов до mount, обращение к запрещённой оси и пустой reveal payload завершаются
101
+ явной диагностической ошибкой.
102
+
103
+ `scrollToBoundary` and `reveal` validate the enabled axes. `reveal` coordinates
104
+ use the physical left/top model and are clamped to the actual scroll range.
105
+ Calls before mount, commands for a disabled axis, and an empty reveal payload
106
+ fail with explicit diagnostics.
107
+
108
+ ## Клавиатура и вложенные controls / Keyboard and nested controls
109
+
110
+ При фокусе на root доступны:
111
+
112
+ When the root owns focus:
113
+
114
+ - `ArrowLeft`/`ArrowRight` — горизонтальная ось;
115
+ - `ArrowUp`/`ArrowDown` — вертикальная ось;
116
+ - `PageUp`/`PageDown` — вертикальная страница, либо горизонтальная при `axis="x"`;
117
+ - `Home`/`End` — начало/конец всех разрешённых осей.
118
+
119
+ Сочетания с `Alt`, `Control`, `Meta` или `Shift` компонент не перехватывает.
120
+ Обработчик действует только когда event target совпадает с root region.
121
+ Вложенные UI-kit inputs, editors, terminals и другие controls сохраняют свои
122
+ клавиши, selection и native copy. Перевод фокуса на вложенный control может
123
+ штатно прокрутить его в видимую область браузером.
124
+
125
+ Combinations with `Alt`, `Control`, `Meta`, or `Shift` are not intercepted. The
126
+ handler runs only when the event target is the root region. Nested UI-kit
127
+ inputs, editors, terminals, and other controls retain their keys, selection,
128
+ and native copy. Moving focus to a nested control may let the browser scroll it
129
+ into view.
130
+
131
+ ## Lifecycle
132
+
133
+ Один `ResizeObserver` отслеживает root viewport и content owner. Passive scroll
134
+ listener и observer освобождаются при unmount; callback после teardown не
135
+ публикует state. Компонент не создаёт global listener и не использует polling.
136
+
137
+ One `ResizeObserver` tracks the root viewport and content owner. The passive
138
+ scroll listener and observer are released on unmount, and callbacks after
139
+ teardown publish no state. The component creates no global listener and uses no
140
+ polling.
141
+
142
+ ## Пример / Example
143
+
144
+ ```vue
145
+ <script setup lang="ts">
146
+ import { ref } from 'vue'
147
+ import SInputText from '@pgcorp/ui-kit/shared/controls/SInputText.vue'
148
+ import SScrollViewport from '@pgcorp/ui-kit/shared/data-display/SScrollViewport.vue'
149
+ import type { SScrollViewportState } from '@pgcorp/ui-kit/shared/data-display/scrollViewport'
150
+
151
+ const query = ref('')
152
+ const position = ref<SScrollViewportState | null>(null)
153
+ </script>
154
+
155
+ <template>
156
+ <SScrollViewport
157
+ accessible-label="Результаты поиска"
158
+ axis="both"
159
+ block-size="lg"
160
+ @scroll-state="position = $event"
161
+ >
162
+ <SInputText v-model="query" label="Поиск" />
163
+ <ResultsMatrix :query="query" />
164
+ </SScrollViewport>
165
+ </template>
166
+ ```
167
+