@pgcorp/ui-kit 0.8.1 → 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.
- package/README.md +332 -9
- package/docs/getting-started.md +135 -0
- package/docs/public-api.md +53 -4
- package/docs/rich-document.md +216 -0
- package/docs/scroll-viewport.md +167 -0
- package/docs/theming.md +17 -0
- package/package.json +108 -50
- package/src/components/layout/SAppShell.vue +3 -1
- package/src/components/layout/SDockRegion.vue +5 -3
- package/src/components/layout/SResizeHandle.vue +45 -5
- package/src/components/layout/SWorkbenchPanelShell.vue +4 -2
- package/src/components/shared/_internal/SDialogSurface.css +23 -0
- package/src/components/shared/_internal/SDialogSurface.vue +9 -2
- package/src/components/shared/_internal/SToastItem.css +4 -0
- package/src/components/shared/_internal/SToastItem.vue +44 -8
- package/src/components/shared/_internal/STreeNode.vue +24 -11
- package/src/components/shared/_internal/dialogSurface.ts +7 -1
- package/src/components/shared/complex/SKanbanLane.vue +4 -2
- package/src/components/shared/containers/SCloudChromeRow.vue +3 -2
- package/src/components/shared/containers/SDrawer.css +11 -2
- package/src/components/shared/containers/SDrawer.vue +48 -9
- package/src/components/shared/containers/SModal.vue +5 -2
- package/src/components/shared/containers/SPanel.vue +4 -2
- package/src/components/shared/containers/SPopover.vue +7 -3
- package/src/components/shared/containers/SUiKitLocaleProvider.vue +57 -0
- package/src/components/shared/controls/SButton.css +14 -10
- package/src/components/shared/controls/SCombobox.vue +8 -6
- package/src/components/shared/controls/SComboboxTrigger.css +7 -4
- package/src/components/shared/controls/SComboboxTrigger.vue +5 -2
- package/src/components/shared/controls/SContextToggleButton.vue +6 -4
- package/src/components/shared/controls/SDragHandle.vue +8 -4
- package/src/components/shared/controls/SDropdownMenu.vue +7 -4
- package/src/components/shared/controls/SInputText.css +5 -4
- package/src/components/shared/controls/SListbox.vue +4 -2
- package/src/components/shared/controls/SSelect.vue +4 -2
- package/src/components/shared/controls/STextarea.ts +13 -0
- package/src/components/shared/controls/STextarea.vue +391 -37
- package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +4 -2
- package/src/components/shared/data-display/SChart.vue +7 -5
- package/src/components/shared/data-display/SChip.vue +3 -1
- package/src/components/shared/data-display/SCodeBlock.css +4 -0
- package/src/components/shared/data-display/SCodeBlock.vue +46 -22
- package/src/components/shared/data-display/SCodeEditor.css +17 -0
- package/src/components/shared/data-display/SCodeEditor.vue +173 -8
- package/src/components/shared/data-display/SCodeSearchPanel.vue +48 -33
- package/src/components/shared/data-display/SCodeWorkspaceEditor.css +13 -0
- package/src/components/shared/data-display/SCodeWorkspaceEditor.vue +355 -0
- package/src/components/shared/data-display/SControlledToastViewport.css +89 -0
- package/src/components/shared/data-display/SControlledToastViewport.vue +205 -0
- package/src/components/shared/data-display/SCopyField.vue +12 -10
- package/src/components/shared/data-display/SDiffViewer.css +122 -0
- package/src/components/shared/data-display/SDiffViewer.vue +499 -0
- package/src/components/shared/data-display/SDocBlock.vue +99 -71
- package/src/components/shared/data-display/SExpandableText.vue +10 -8
- package/src/components/shared/data-display/SJsonTree.vue +4 -2
- package/src/components/shared/data-display/SLinkedSystemsList.vue +4 -2
- package/src/components/shared/data-display/SMessage.vue +6 -6
- package/src/components/shared/data-display/SRichDocument.css +8 -0
- package/src/components/shared/data-display/SRichDocument.vue +65 -0
- package/src/components/shared/data-display/SScrollViewport.css +53 -0
- package/src/components/shared/data-display/SScrollViewport.vue +470 -0
- package/src/components/shared/data-display/STable.vue +38 -19
- package/src/components/shared/data-display/SToastContainer.vue +22 -17
- package/src/components/shared/data-display/SVirtualList.ts +33 -0
- package/src/components/shared/data-display/SVirtualList.vue +321 -36
- package/src/components/shared/data-display/_internal/SCodeTokenLine.css +72 -0
- package/src/components/shared/data-display/_internal/SCodeTokenLine.vue +79 -0
- package/src/components/shared/data-display/_internal/SRichBlockNode.css +85 -0
- package/src/components/shared/data-display/_internal/SRichBlockNode.vue +282 -0
- package/src/components/shared/data-display/_internal/SRichInlineNode.css +6 -0
- package/src/components/shared/data-display/_internal/SRichInlineNode.vue +76 -0
- package/src/components/shared/data-display/_internal/SRichMath.css +42 -0
- package/src/components/shared/data-display/_internal/SRichMath.vue +102 -0
- package/src/components/shared/data-display/codeEditor.ts +200 -0
- package/src/components/shared/data-display/codeWorkspace.ts +233 -0
- package/src/components/shared/data-display/diff.ts +541 -0
- package/src/components/shared/data-display/richDocument.ts +876 -0
- package/src/components/shared/data-display/scrollViewport.ts +47 -0
- package/src/components/shared/data-display/toast.ts +194 -0
- package/src/components/shared/database/SDataGrid.vue +7 -4
- package/src/components/shared/database/SSqlEditor.vue +39 -39
- package/src/components/shared/feedback/SAsyncState.vue +3 -1
- package/src/components/shared/graph/SGraphViewport.vue +11 -9
- package/src/components/shared/navigation/SBreadcrumbs.vue +8 -5
- package/src/components/shared/navigation/SCatalogNavigator.vue +26 -20
- package/src/components/shared/navigation/STabList.vue +10 -6
- package/src/components/shared/navigation/STabs.vue +15 -11
- package/src/components/shared/navigation/SWizardSteps.vue +4 -2
- package/src/components/shared/persona/SPersonaProjectPicker.vue +31 -27
- package/src/components/shared/persona/SPersonaRunWorkbench.vue +39 -33
- package/src/components/shared/persona/_internal/SPersonaRunConsole.vue +8 -6
- package/src/components/shared/persona/_internal/labels.ts +66 -57
- package/src/components/shared/persona/presentation.ts +140 -110
- package/src/composables/useFloatingPosition.ts +39 -2
- package/src/composables/useUiKitLocale.ts +17 -0
- package/src/internal/codeHighlight.ts +110 -0
- package/src/internal/diffEngine.ts +464 -0
- package/src/internal/diffRuntime.ts +189 -0
- package/src/internal/feedbackPresentation.ts +33 -14
- package/src/internal/localeContext.ts +22 -0
- package/src/internal/ownedAttrs.ts +8 -0
- package/src/internal/runtimeContract.ts +11 -9
- package/src/internal/semanticSizing.ts +13 -0
- package/src/locales/catalog.ts +634 -0
- package/src/locales/locale.ts +472 -0
- package/src/locales/messages.generated.ts +1826 -0
- package/src/locales/messages.ts +150 -0
- package/src/stores/useNotifierStore.ts +61 -44
- package/src/styles/tokens.css +23 -2
- package/src/theme.ts +59 -32
- package/src/workers/diff.worker.ts +73 -0
- package/src/workers/diffProtocol.ts +155 -0
- 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"
|
|
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:
|
|
90
|
+
? { status: 'loading', message: resolvedLoadingMessage }
|
|
91
91
|
: errorMessage
|
|
92
92
|
? { status: 'error', message: errorMessage }
|
|
93
|
-
: { status: 'empty', message:
|
|
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="
|
|
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
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
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
|
|
604
|
-
if (direction === 'descending') return
|
|
605
|
-
return
|
|
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
|
|
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
|
|
870
|
-
return rowIsSelected(entry)
|
|
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
|
-
|
|
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
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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 {
|
|
18
|
-
import
|
|
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
|
|
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(
|
|
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>
|