@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,470 @@
1
+ <template>
2
+ <div
3
+ v-bind="ownedAttrs.bindings()"
4
+ ref="viewportRef"
5
+ class="s-scroll-viewport"
6
+ role="region"
7
+ :aria-label="contract.accessibleLabel"
8
+ :tabindex="contract.focusable ? 0 : undefined"
9
+ :data-axis="contract.axis"
10
+ :data-block-size="contract.blockSize"
11
+ :data-focusable="contract.focusable"
12
+ :data-keyboard-navigation="contract.keyboardNavigation"
13
+ :data-overscroll-behavior="contract.overscrollBehavior"
14
+ :data-at-inline-start="viewportState?.atInlineStart"
15
+ :data-at-inline-end="viewportState?.atInlineEnd"
16
+ :data-at-block-start="viewportState?.atBlockStart"
17
+ :data-at-block-end="viewportState?.atBlockEnd"
18
+ @keydown="onKeydown"
19
+ >
20
+ <div ref="contentRef" class="s-scroll-viewport__content">
21
+ <slot />
22
+ </div>
23
+ </div>
24
+ </template>
25
+
26
+ <script lang="ts">
27
+ import type { ScrollViewportBlockSize as PublicScrollViewportBlockSize } from '../../../internal/semanticSizing'
28
+ import type {
29
+ SScrollViewportAxis as PublicSScrollViewportAxis,
30
+ SScrollViewportOverscrollBehavior as PublicSScrollViewportOverscrollBehavior,
31
+ } from './scrollViewport'
32
+
33
+ export interface Props {
34
+ accessibleLabel: string
35
+ axis?: PublicSScrollViewportAxis
36
+ blockSize?: PublicScrollViewportBlockSize
37
+ focusable?: boolean
38
+ keyboardNavigation?: boolean
39
+ overscrollBehavior?: PublicSScrollViewportOverscrollBehavior
40
+ }
41
+ </script>
42
+
43
+ <script setup lang="ts">
44
+ import {
45
+ computed,
46
+ nextTick,
47
+ onBeforeUnmount,
48
+ onMounted,
49
+ shallowRef,
50
+ watch,
51
+ } from 'vue'
52
+ import { useOwnedAttrs } from '../../../internal/ownedAttrs'
53
+ import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
54
+ import {
55
+ validateBoolean,
56
+ validateBoundedNumber,
57
+ validateExactString,
58
+ validateNonEmptyString,
59
+ } from '../../../internal/runtimeContract'
60
+ import { validateScrollViewportBlockSize } from '../../../internal/semanticSizing'
61
+ import type {
62
+ SScrollViewportApi,
63
+ SScrollViewportBehavior,
64
+ SScrollViewportBoundary,
65
+ SScrollViewportPhysicalAxis,
66
+ SScrollViewportRevealOptions,
67
+ SScrollViewportState,
68
+ } from './scrollViewport'
69
+
70
+ defineOptions({ inheritAttrs: false })
71
+
72
+ const props = withDefaults(defineProps<Props>(), {
73
+ axis: 'y',
74
+ blockSize: 'md',
75
+ focusable: true,
76
+ keyboardNavigation: true,
77
+ overscrollBehavior: 'contain',
78
+ })
79
+
80
+ const emit = defineEmits<{
81
+ 'scroll-state': [state: SScrollViewportState]
82
+ }>()
83
+
84
+ const viewportRef = shallowRef<HTMLElement | null>(null)
85
+ const contentRef = shallowRef<HTMLElement | null>(null)
86
+ const viewportState = shallowRef<SScrollViewportState | null>(null)
87
+ const ownedAttrs = useOwnedAttrs({ component: 'SScrollViewport', owner: 'scroll viewport region' })
88
+ let resizeObserver: ResizeObserver | null = null
89
+ let lastStateSignature = ''
90
+ let rtlScrollType: 'default' | 'negative' | 'reverse' | null = null
91
+
92
+ const contract = computed(() => {
93
+ const focusable = validateBoolean('SScrollViewport', 'focusable', props.focusable)
94
+ const keyboardNavigation = validateBoolean(
95
+ 'SScrollViewport',
96
+ 'keyboardNavigation',
97
+ props.keyboardNavigation,
98
+ )
99
+ if (keyboardNavigation && !focusable) {
100
+ throw new Error(
101
+ 'SScrollViewport: keyboardNavigation=true требует focusable=true. '
102
+ + '/ SScrollViewport: keyboardNavigation=true requires focusable=true.',
103
+ )
104
+ }
105
+ return {
106
+ accessibleLabel: validateNonEmptyString(
107
+ 'SScrollViewport',
108
+ 'accessibleLabel',
109
+ props.accessibleLabel,
110
+ ),
111
+ axis: validateExactString(
112
+ 'SScrollViewport',
113
+ 'axis',
114
+ props.axis,
115
+ ['x', 'y', 'both'] as const,
116
+ ),
117
+ blockSize: validateScrollViewportBlockSize(
118
+ 'SScrollViewport',
119
+ 'blockSize',
120
+ props.blockSize,
121
+ ),
122
+ focusable,
123
+ keyboardNavigation,
124
+ overscrollBehavior: validateExactString(
125
+ 'SScrollViewport',
126
+ 'overscrollBehavior',
127
+ props.overscrollBehavior,
128
+ ['auto', 'contain', 'none'] as const,
129
+ ),
130
+ }
131
+ })
132
+
133
+ useInteractiveLeafRegistration({
134
+ owner: 'SScrollViewport',
135
+ active: () => contract.value.focusable,
136
+ })
137
+
138
+ function clamp(value: number, maximum: number): number {
139
+ return Math.min(maximum, Math.max(0, value))
140
+ }
141
+
142
+ function detectRtlScrollType(element: HTMLElement): 'default' | 'negative' | 'reverse' {
143
+ if (rtlScrollType) return rtlScrollType
144
+ const document = element.ownerDocument
145
+ const probe = document.createElement('div')
146
+ const content = document.createElement('div')
147
+ probe.dir = 'rtl'
148
+ probe.style.cssText = 'position:absolute;inset:-9999px;width:4px;height:1px;overflow:scroll;'
149
+ content.style.cssText = 'width:8px;height:1px;'
150
+ probe.append(content)
151
+ document.body.append(probe)
152
+ if (probe.scrollLeft > 0) {
153
+ rtlScrollType = 'default'
154
+ } else {
155
+ probe.scrollLeft = 1
156
+ rtlScrollType = probe.scrollLeft === 0 ? 'negative' : 'reverse'
157
+ }
158
+ probe.remove()
159
+ return rtlScrollType
160
+ }
161
+
162
+ function physicalLeftOriginOffset(element: HTMLElement, maximum: number): number {
163
+ const scrollLeft = element.scrollLeft
164
+ if (getComputedStyle(element).direction !== 'rtl') return clamp(scrollLeft, maximum)
165
+ switch (detectRtlScrollType(element)) {
166
+ case 'negative':
167
+ return clamp(maximum + scrollLeft, maximum)
168
+ case 'reverse':
169
+ return clamp(maximum - scrollLeft, maximum)
170
+ case 'default':
171
+ return clamp(scrollLeft, maximum)
172
+ }
173
+ }
174
+
175
+ function browserScrollLeft(element: HTMLElement, physicalX: number, maximum: number): number {
176
+ const x = clamp(physicalX, maximum)
177
+ if (getComputedStyle(element).direction !== 'rtl') return x
178
+ switch (detectRtlScrollType(element)) {
179
+ case 'negative':
180
+ return x - maximum
181
+ case 'reverse':
182
+ return maximum - x
183
+ case 'default':
184
+ return x
185
+ }
186
+ }
187
+
188
+ function semanticOffsets(
189
+ element: HTMLElement,
190
+ physical: SScrollViewportState['physical'],
191
+ ): SScrollViewportState['semantic'] {
192
+ const styles = getComputedStyle(element)
193
+ const verticalWriting = styles.writingMode.startsWith('vertical')
194
+ || styles.writingMode.startsWith('sideways')
195
+ if (!verticalWriting) {
196
+ return Object.freeze({
197
+ inline: styles.direction === 'rtl' ? physical.maxX - physical.x : physical.x,
198
+ block: physical.y,
199
+ maxInline: physical.maxX,
200
+ maxBlock: physical.maxY,
201
+ })
202
+ }
203
+ return Object.freeze({
204
+ inline: styles.direction === 'rtl'
205
+ ? physical.maxY - physical.y
206
+ : physical.y,
207
+ block: styles.writingMode.endsWith('-rl') ? physical.maxX - physical.x : physical.x,
208
+ maxInline: physical.maxY,
209
+ maxBlock: physical.maxX,
210
+ })
211
+ }
212
+
213
+ function readState(element: HTMLElement): SScrollViewportState {
214
+ const maxX = Math.max(0, element.scrollWidth - element.clientWidth)
215
+ const maxY = Math.max(0, element.scrollHeight - element.clientHeight)
216
+ const physical = Object.freeze({
217
+ x: physicalLeftOriginOffset(element, maxX),
218
+ y: clamp(element.scrollTop, maxY),
219
+ maxX,
220
+ maxY,
221
+ })
222
+ const semantic = semanticOffsets(element, physical)
223
+ const epsilon = 1
224
+ return Object.freeze({
225
+ physical,
226
+ semantic,
227
+ atInlineStart: semantic.inline <= epsilon,
228
+ atInlineEnd: semantic.maxInline - semantic.inline <= epsilon,
229
+ atBlockStart: semantic.block <= epsilon,
230
+ atBlockEnd: semantic.maxBlock - semantic.block <= epsilon,
231
+ })
232
+ }
233
+
234
+ function publishScrollState(): void {
235
+ const element = viewportRef.value
236
+ if (!element) return
237
+ const state = readState(element)
238
+ const signature = JSON.stringify(state)
239
+ if (signature === lastStateSignature) return
240
+ lastStateSignature = signature
241
+ viewportState.value = state
242
+ emit('scroll-state', state)
243
+ }
244
+
245
+ function requireViewport(action: string): HTMLElement {
246
+ const element = viewportRef.value
247
+ if (element) return element
248
+ throw new Error(
249
+ `SScrollViewport: ${action} недоступен до mount. `
250
+ + `/ SScrollViewport: ${action} is unavailable before mount.`,
251
+ )
252
+ }
253
+
254
+ function supportsAxis(axis: SScrollViewportPhysicalAxis): boolean {
255
+ return contract.value.axis === axis || contract.value.axis === 'both'
256
+ }
257
+
258
+ function assertSupportedAxis(axis: SScrollViewportPhysicalAxis, action: string): void {
259
+ if (supportsAxis(axis)) return
260
+ throw new Error(
261
+ `SScrollViewport: ${action} не может управлять осью ${axis} при axis=${contract.value.axis}. `
262
+ + `/ SScrollViewport: ${action} cannot control axis ${axis} when axis=${contract.value.axis}.`,
263
+ )
264
+ }
265
+
266
+ function focus(): void {
267
+ if (!contract.value.focusable) {
268
+ throw new Error(
269
+ 'SScrollViewport: focus() требует focusable=true. '
270
+ + '/ SScrollViewport: focus() requires focusable=true.',
271
+ )
272
+ }
273
+ requireViewport('focus()').focus()
274
+ }
275
+
276
+ function scrollToBoundary(
277
+ axis: SScrollViewportPhysicalAxis,
278
+ boundary: SScrollViewportBoundary,
279
+ ): void {
280
+ const validatedAxis = validateExactString(
281
+ 'SScrollViewport',
282
+ 'scrollToBoundary.axis',
283
+ axis,
284
+ ['x', 'y'] as const,
285
+ )
286
+ const validatedBoundary = validateExactString(
287
+ 'SScrollViewport',
288
+ 'scrollToBoundary.boundary',
289
+ boundary,
290
+ ['start', 'end'] as const,
291
+ )
292
+ assertSupportedAxis(validatedAxis, 'scrollToBoundary()')
293
+ const element = requireViewport('scrollToBoundary()')
294
+ const current = readState(element).physical
295
+ const x = validatedAxis === 'x'
296
+ ? (validatedBoundary === 'start' ? 0 : current.maxX)
297
+ : current.x
298
+ const y = validatedAxis === 'y'
299
+ ? (validatedBoundary === 'start' ? 0 : current.maxY)
300
+ : current.y
301
+ element.scrollTo({
302
+ left: browserScrollLeft(element, x, current.maxX),
303
+ top: y,
304
+ behavior: 'auto',
305
+ })
306
+ publishScrollState()
307
+ }
308
+
309
+ function reveal(options: SScrollViewportRevealOptions): void {
310
+ if (typeof options !== 'object' || options === null || Array.isArray(options)) {
311
+ throw new TypeError(
312
+ 'SScrollViewport: reveal.options должен быть объектом. '
313
+ + '/ SScrollViewport: reveal.options must be an object.',
314
+ )
315
+ }
316
+ if (options.x === undefined && options.y === undefined) {
317
+ throw new Error(
318
+ 'SScrollViewport: reveal() требует x и/или y. '
319
+ + '/ SScrollViewport: reveal() requires x and/or y.',
320
+ )
321
+ }
322
+ if (options.x !== undefined) assertSupportedAxis('x', 'reveal()')
323
+ if (options.y !== undefined) assertSupportedAxis('y', 'reveal()')
324
+ const element = requireViewport('reveal()')
325
+ const current = readState(element).physical
326
+ const x = options.x === undefined
327
+ ? current.x
328
+ : validateBoundedNumber(
329
+ 'SScrollViewport',
330
+ 'reveal.x',
331
+ options.x,
332
+ { minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
333
+ )
334
+ const y = options.y === undefined
335
+ ? current.y
336
+ : validateBoundedNumber(
337
+ 'SScrollViewport',
338
+ 'reveal.y',
339
+ options.y,
340
+ { minimum: 0, maximum: Number.MAX_SAFE_INTEGER },
341
+ )
342
+ const behavior: SScrollViewportBehavior = validateExactString(
343
+ 'SScrollViewport',
344
+ 'reveal.behavior',
345
+ options.behavior ?? 'auto',
346
+ ['auto', 'smooth'] as const,
347
+ )
348
+ element.scrollTo({
349
+ left: browserScrollLeft(element, x, current.maxX),
350
+ top: clamp(y, current.maxY),
351
+ behavior,
352
+ })
353
+ publishScrollState()
354
+ }
355
+
356
+ function keyboardTarget(
357
+ element: HTMLElement,
358
+ event: KeyboardEvent,
359
+ ): { left: number; top: number } | null {
360
+ const horizontal = supportsAxis('x')
361
+ const vertical = supportsAxis('y')
362
+ const horizontalStep = Math.max(1, element.clientWidth / 8)
363
+ const verticalStep = Math.max(1, element.clientHeight / 8)
364
+ const pageX = Math.max(1, element.clientWidth * 0.9)
365
+ const pageY = Math.max(1, element.clientHeight * 0.9)
366
+ const current = readState(element).physical
367
+ switch (event.key) {
368
+ case 'ArrowLeft':
369
+ return horizontal ? { left: current.x - horizontalStep, top: current.y } : null
370
+ case 'ArrowRight':
371
+ return horizontal ? { left: current.x + horizontalStep, top: current.y } : null
372
+ case 'ArrowUp':
373
+ return vertical ? { left: current.x, top: current.y - verticalStep } : null
374
+ case 'ArrowDown':
375
+ return vertical ? { left: current.x, top: current.y + verticalStep } : null
376
+ case 'PageUp':
377
+ return vertical
378
+ ? { left: current.x, top: current.y - pageY }
379
+ : horizontal ? { left: current.x - pageX, top: current.y } : null
380
+ case 'PageDown':
381
+ return vertical
382
+ ? { left: current.x, top: current.y + pageY }
383
+ : horizontal ? { left: current.x + pageX, top: current.y } : null
384
+ case 'Home':
385
+ return { left: horizontal ? 0 : current.x, top: vertical ? 0 : current.y }
386
+ case 'End':
387
+ return {
388
+ left: horizontal ? current.maxX : current.x,
389
+ top: vertical ? current.maxY : current.y,
390
+ }
391
+ default:
392
+ return null
393
+ }
394
+ }
395
+
396
+ function onKeydown(event: KeyboardEvent): void {
397
+ const element = viewportRef.value
398
+ if (
399
+ !element
400
+ || !contract.value.keyboardNavigation
401
+ || event.target !== element
402
+ || event.defaultPrevented
403
+ || event.altKey
404
+ || event.ctrlKey
405
+ || event.metaKey
406
+ || event.shiftKey
407
+ ) return
408
+ const target = keyboardTarget(element, event)
409
+ if (!target) return
410
+ event.preventDefault()
411
+ const current = readState(element).physical
412
+ element.scrollTo({
413
+ left: browserScrollLeft(element, target.left, current.maxX),
414
+ top: clamp(target.top, current.maxY),
415
+ behavior: 'auto',
416
+ })
417
+ publishScrollState()
418
+ }
419
+
420
+ function onScroll(): void {
421
+ publishScrollState()
422
+ }
423
+
424
+ onMounted(() => {
425
+ const viewport = requireViewport('observer setup')
426
+ const content = contentRef.value
427
+ if (!content) {
428
+ throw new Error(
429
+ 'SScrollViewport: content owner отсутствует после mount. '
430
+ + '/ SScrollViewport: content owner is missing after mount.',
431
+ )
432
+ }
433
+ if (typeof ResizeObserver === 'undefined') {
434
+ throw new Error(
435
+ 'SScrollViewport: runtime требует ResizeObserver. '
436
+ + '/ SScrollViewport: runtime requires ResizeObserver.',
437
+ )
438
+ }
439
+ viewport.addEventListener('scroll', onScroll, { passive: true })
440
+ resizeObserver = new ResizeObserver(publishScrollState)
441
+ resizeObserver.observe(viewport)
442
+ resizeObserver.observe(content)
443
+ publishScrollState()
444
+ })
445
+
446
+ watch(
447
+ () => [
448
+ contract.value.axis,
449
+ contract.value.blockSize,
450
+ contract.value.focusable,
451
+ contract.value.keyboardNavigation,
452
+ contract.value.overscrollBehavior,
453
+ ],
454
+ async () => {
455
+ await nextTick()
456
+ publishScrollState()
457
+ },
458
+ )
459
+
460
+ onBeforeUnmount(() => {
461
+ viewportRef.value?.removeEventListener('scroll', onScroll)
462
+ resizeObserver?.disconnect()
463
+ resizeObserver = null
464
+ lastStateSignature = ''
465
+ })
466
+
467
+ defineExpose<SScrollViewportApi>({ focus, scrollToBoundary, reveal })
468
+ </script>
469
+
470
+ <style lang="postcss" src="./SScrollViewport.css" scoped></style>
@@ -26,7 +26,7 @@
26
26
  :model-value="allCurrentRowsSelected"
27
27
  :indeterminate="someCurrentRowsSelected && !allCurrentRowsSelected"
28
28
  :disabled="selectableRows.length === 0"
29
- :aria-label="allCurrentRowsSelected ? 'Снять выбор со всех строк' : 'Выбрать все строки'"
29
+ :aria-label="allCurrentRowsSelected ? t('table.clearAllRows') : t('table.selectAllRows')"
30
30
  @update:model-value="updateAllRowsSelection"
31
31
  />
32
32
  </th>
@@ -36,7 +36,7 @@
36
36
  scope="col"
37
37
  data-width="2xs"
38
38
  >
39
- <span class="s-table__assistive">Детали строки</span>
39
+ <span class="s-table__assistive">{{ t('table.rowDetails') }}</span>
40
40
  </th>
41
41
  <th
42
42
  v-for="column in validatedColumns"
@@ -87,10 +87,10 @@
87
87
  <SAsyncState
88
88
  v-if="validatedLoading || errorMessage || validatedRows.length === 0"
89
89
  :state="validatedLoading
90
- ? { status: 'loading', message: loadingMessage }
90
+ ? { status: 'loading', message: resolvedLoadingMessage }
91
91
  : errorMessage
92
92
  ? { status: 'error', message: errorMessage }
93
- : { status: 'empty', message: emptyMessage }"
93
+ : { status: 'empty', message: resolvedEmptyMessage }"
94
94
  presentation="table-row"
95
95
  :colspan="renderedColumnCount"
96
96
  />
@@ -169,7 +169,7 @@
169
169
  :id="rowDetailsId(entry.key)"
170
170
  class="s-table__row-details"
171
171
  role="region"
172
- :aria-label="`Детали: ${resolveRowLabel(entry.row, entry.index)}`"
172
+ :aria-label="t('table.detailsFor', { label: resolveRowLabel(entry.row, entry.index) })"
173
173
  >
174
174
  <slot name="row-details" :row="entry.row" :row-index="entry.index" />
175
175
  </div>
@@ -256,6 +256,7 @@ export interface STableProps<
256
256
 
257
257
  <script setup lang="ts" generic="Row extends object = Record<string, unknown>, RowKey extends STableRowKey = STableRowKey, Field extends string = Extract<keyof Row, string>">
258
258
  import { computed, useId, useSlots, watch } from 'vue'
259
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
259
260
  import { ChevronDown, ChevronsUpDown, ChevronUp } from '../../icons/sputnigUiIcons'
260
261
  import { hasOwn } from '../../../internal/es2020'
261
262
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
@@ -278,9 +279,9 @@ defineOptions({ inheritAttrs: false })
278
279
 
279
280
  const props = withDefaults(defineProps<STableProps<Row, RowKey, Field>>(), {
280
281
  loading: false,
281
- loadingMessage: 'Загрузка...',
282
+ loadingMessage: undefined,
282
283
  errorMessage: '',
283
- emptyMessage: 'Нет данных для отображения',
284
+ emptyMessage: undefined,
284
285
  selectedRowKey: undefined,
285
286
  selectionMode: 'none',
286
287
  selectedRowKeys: () => [],
@@ -313,6 +314,7 @@ const emit = defineEmits<{
313
314
  'multi-sort': [value: readonly STableSort<Field>[]]
314
315
  }>()
315
316
  const slots = useSlots()
317
+ const { t } = useUiKitLocale()
316
318
 
317
319
  const TABLE_DENSITIES = ['default', 'compact'] as const
318
320
  const TABLE_SURFACES = ['bordered', 'plain'] as const
@@ -352,6 +354,8 @@ function onRowKeydown(event: KeyboardEvent, row: Row, rowIndex: number): void {
352
354
  emit('row-click', row, rowIndex)
353
355
  }
354
356
  const validatedLoading = computed(() => validateBoolean('STable', 'loading', props.loading))
357
+ const resolvedLoadingMessage = computed(() => props.loadingMessage ?? t('table.loading'))
358
+ const resolvedEmptyMessage = computed(() => props.emptyMessage ?? t('table.empty'))
355
359
  const resolvedAccessibleName = computed(() => {
356
360
  if (props.ariaLabel !== undefined && props.ariaLabelledby !== undefined) {
357
361
  throw new TypeError('STable: задайте только ariaLabel или ariaLabelledby. / STable: provide only ariaLabel or ariaLabelledby.')
@@ -364,7 +368,7 @@ const resolvedAccessibleName = computed(() => {
364
368
  }
365
369
  return {
366
370
  ariaLabel: props.ariaLabel === undefined
367
- ? 'Таблица данных'
371
+ ? t('table.accessibleName')
368
372
  : validateNonEmptyString('STable', 'ariaLabel', props.ariaLabel),
369
373
  ariaLabelledby: undefined,
370
374
  }
@@ -594,15 +598,25 @@ const sortActionLabel = (column: STableColumn<Row, Field>): string => {
594
598
  const index = sortIndex(column)
595
599
  const current = index === -1 ? undefined : activeSorts.value[index]
596
600
  if (validatedSortMode.value === 'multiple') {
597
- if (!current) return `${column.header}: добавить в сортировку по возрастанию`
598
- const state = current.direction === 'ascending' ? 'по возрастанию' : 'по убыванию'
599
- const action = current.direction === 'ascending' ? 'сортировать по убыванию' : 'убрать из сортировки'
600
- return `${column.header}: приоритет ${index + 1} из ${activeSorts.value.length}, ${state}; ${action}`
601
+ if (!current) return t('table.addSortAscending', { column: column.header })
602
+ const direction = current.direction === 'ascending'
603
+ ? t('table.ascending')
604
+ : t('table.descending')
605
+ const action = current.direction === 'ascending'
606
+ ? t('table.sortNextDescending')
607
+ : t('table.removeFromSort')
608
+ return t('table.multiSortSummary', {
609
+ column: column.header,
610
+ priority: index + 1,
611
+ total: activeSorts.value.length,
612
+ direction,
613
+ action,
614
+ })
601
615
  }
602
616
  const direction = current?.direction
603
- if (direction === 'ascending') return `${column.header}: сортировать по убыванию`
604
- if (direction === 'descending') return `${column.header}: сбросить сортировку`
605
- return `${column.header}: сортировать по возрастанию`
617
+ if (direction === 'ascending') return t('table.sortDescending', { column: column.header })
618
+ if (direction === 'descending') return t('table.clearSort', { column: column.header })
619
+ return t('table.sortAscending', { column: column.header })
606
620
  }
607
621
 
608
622
  function requestSort(column: STableColumn<Row, Field>): void {
@@ -831,7 +845,7 @@ function rowDetailsId(key: RowKey): string {
831
845
  }
832
846
 
833
847
  function resolveRowLabel(row: Row, index: number): string {
834
- if (props.getRowLabel === undefined) return `Строка ${index + 1}`
848
+ if (props.getRowLabel === undefined) return t('table.rowLabel', { index: index + 1 })
835
849
  if (typeof props.getRowLabel !== 'function') {
836
850
  throw new TypeError(`${diagnosticOwner()}: getRowLabel must be a function.`)
837
851
  }
@@ -866,8 +880,10 @@ function rowAriaSelected(entry: ResolvedRowEntry): 'true' | 'false' | undefined
866
880
 
867
881
  function rowSelectionLabel(entry: ResolvedRowEntry): string {
868
882
  const label = resolveRowLabel(entry.row, entry.index)
869
- if (!entry.selectable) return `Выбор недоступен: ${label}`
870
- return rowIsSelected(entry) ? `Снять выбор: ${label}` : `Выбрать: ${label}`
883
+ if (!entry.selectable) return t('table.selectionUnavailable', { label })
884
+ return rowIsSelected(entry)
885
+ ? t('table.deselectRow', { label })
886
+ : t('table.selectRow', { label })
871
887
  }
872
888
 
873
889
  function updateRowSelection(entry: ResolvedRowEntry, selected: boolean): void {
@@ -900,7 +916,10 @@ function rowIsExpanded(entry: ResolvedRowEntry): boolean {
900
916
  }
901
917
 
902
918
  function rowExpansionLabel(entry: ResolvedRowEntry): string {
903
- return `${rowIsExpanded(entry) ? 'Свернуть' : 'Раскрыть'}: ${resolveRowLabel(entry.row, entry.index)}`
919
+ const label = resolveRowLabel(entry.row, entry.index)
920
+ return rowIsExpanded(entry)
921
+ ? t('table.collapseRow', { label })
922
+ : t('table.expandRow', { label })
904
923
  }
905
924
 
906
925
  function toggleRowExpansion(entry: ResolvedRowEntry): void {
@@ -1,31 +1,36 @@
1
1
  <template>
2
- <div v-bind="ownedAttrs.bindings()" class="s-toast-container">
3
- <TransitionGroup name="notification-transition" tag="div" class="s-toast-transition-wrapper">
4
- <SToastItem
5
- v-for="notification in notifications"
6
- :key="notification.id"
7
- :notification="notification"
8
- @dismiss="dismiss"
9
- />
10
- </TransitionGroup>
11
- </div>
2
+ <SControlledToastViewport
3
+ v-bind="ownedAttrs.bindings()"
4
+ :notifications="notifications"
5
+ :accessible-label="t('feedback.notifications')"
6
+ lifetime-owner="consumer"
7
+ :max-visible="Math.max(1, notifications.length)"
8
+ @dismiss="dismiss"
9
+ />
12
10
  </template>
13
11
 
14
12
  <script setup lang="ts">
15
13
  import { useNotifierStore } from '../../../stores/useNotifierStore'
16
14
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
17
- import { storeToRefs } from 'pinia'
18
- import SToastItem from '../_internal/SToastItem.vue'
15
+ import { computed } from 'vue'
16
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
17
+ import SControlledToastViewport from './SControlledToastViewport.vue'
18
+ import type { SToastDismissIntent, SToastPresentation } from './toast'
19
19
 
20
20
  defineOptions({ inheritAttrs: false })
21
21
 
22
22
  const store = useNotifierStore()
23
- const { notifications } = storeToRefs(store)
23
+ const notifications = computed<readonly SToastPresentation[]>(() =>
24
+ store.notifications.map((notification) => ({
25
+ id: notification.id,
26
+ severity: notification.severity,
27
+ title: notification.title,
28
+ message: notification.message,
29
+ })))
24
30
  const ownedAttrs = useOwnedAttrs({ component: 'SToastContainer', owner: 'toast container root' })
31
+ const { t } = useUiKitLocale()
25
32
 
26
- function dismiss(id: string): void {
27
- store.remove(id)
33
+ function dismiss(intent: SToastDismissIntent): void {
34
+ store.remove(intent.id)
28
35
  }
29
36
  </script>
30
-
31
- <style lang="postcss" src="./SToastContainer.css" scoped></style>