@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.
Files changed (113) hide show
  1. package/README.md +332 -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/docs/theming.md +17 -0
  7. package/package.json +108 -50
  8. package/src/components/layout/SAppShell.vue +3 -1
  9. package/src/components/layout/SDockRegion.vue +5 -3
  10. package/src/components/layout/SResizeHandle.vue +45 -5
  11. package/src/components/layout/SWorkbenchPanelShell.vue +4 -2
  12. package/src/components/shared/_internal/SDialogSurface.css +23 -0
  13. package/src/components/shared/_internal/SDialogSurface.vue +9 -2
  14. package/src/components/shared/_internal/SToastItem.css +4 -0
  15. package/src/components/shared/_internal/SToastItem.vue +44 -8
  16. package/src/components/shared/_internal/STreeNode.vue +24 -11
  17. package/src/components/shared/_internal/dialogSurface.ts +7 -1
  18. package/src/components/shared/complex/SKanbanLane.vue +4 -2
  19. package/src/components/shared/containers/SCloudChromeRow.vue +3 -2
  20. package/src/components/shared/containers/SDrawer.css +11 -2
  21. package/src/components/shared/containers/SDrawer.vue +48 -9
  22. package/src/components/shared/containers/SModal.vue +5 -2
  23. package/src/components/shared/containers/SPanel.vue +4 -2
  24. package/src/components/shared/containers/SPopover.vue +7 -3
  25. package/src/components/shared/containers/SUiKitLocaleProvider.vue +57 -0
  26. package/src/components/shared/controls/SButton.css +14 -10
  27. package/src/components/shared/controls/SCombobox.vue +8 -6
  28. package/src/components/shared/controls/SComboboxTrigger.css +7 -4
  29. package/src/components/shared/controls/SComboboxTrigger.vue +5 -2
  30. package/src/components/shared/controls/SContextToggleButton.vue +6 -4
  31. package/src/components/shared/controls/SDragHandle.vue +8 -4
  32. package/src/components/shared/controls/SDropdownMenu.vue +7 -4
  33. package/src/components/shared/controls/SInputText.css +5 -4
  34. package/src/components/shared/controls/SListbox.vue +4 -2
  35. package/src/components/shared/controls/SSelect.vue +4 -2
  36. package/src/components/shared/controls/STextarea.ts +13 -0
  37. package/src/components/shared/controls/STextarea.vue +391 -37
  38. package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +4 -2
  39. package/src/components/shared/data-display/SChart.vue +7 -5
  40. package/src/components/shared/data-display/SChip.vue +3 -1
  41. package/src/components/shared/data-display/SCodeBlock.css +4 -0
  42. package/src/components/shared/data-display/SCodeBlock.vue +46 -22
  43. package/src/components/shared/data-display/SCodeEditor.css +17 -0
  44. package/src/components/shared/data-display/SCodeEditor.vue +173 -8
  45. package/src/components/shared/data-display/SCodeSearchPanel.vue +48 -33
  46. package/src/components/shared/data-display/SCodeWorkspaceEditor.css +13 -0
  47. package/src/components/shared/data-display/SCodeWorkspaceEditor.vue +355 -0
  48. package/src/components/shared/data-display/SControlledToastViewport.css +89 -0
  49. package/src/components/shared/data-display/SControlledToastViewport.vue +205 -0
  50. package/src/components/shared/data-display/SCopyField.vue +12 -10
  51. package/src/components/shared/data-display/SDiffViewer.css +122 -0
  52. package/src/components/shared/data-display/SDiffViewer.vue +499 -0
  53. package/src/components/shared/data-display/SDocBlock.vue +99 -71
  54. package/src/components/shared/data-display/SExpandableText.vue +10 -8
  55. package/src/components/shared/data-display/SJsonTree.vue +4 -2
  56. package/src/components/shared/data-display/SLinkedSystemsList.vue +4 -2
  57. package/src/components/shared/data-display/SMessage.vue +6 -6
  58. package/src/components/shared/data-display/SRichDocument.css +8 -0
  59. package/src/components/shared/data-display/SRichDocument.vue +65 -0
  60. package/src/components/shared/data-display/SScrollViewport.css +53 -0
  61. package/src/components/shared/data-display/SScrollViewport.vue +470 -0
  62. package/src/components/shared/data-display/STable.vue +38 -19
  63. package/src/components/shared/data-display/SToastContainer.vue +22 -17
  64. package/src/components/shared/data-display/SVirtualList.ts +33 -0
  65. package/src/components/shared/data-display/SVirtualList.vue +321 -36
  66. package/src/components/shared/data-display/_internal/SCodeTokenLine.css +72 -0
  67. package/src/components/shared/data-display/_internal/SCodeTokenLine.vue +79 -0
  68. package/src/components/shared/data-display/_internal/SRichBlockNode.css +85 -0
  69. package/src/components/shared/data-display/_internal/SRichBlockNode.vue +282 -0
  70. package/src/components/shared/data-display/_internal/SRichInlineNode.css +6 -0
  71. package/src/components/shared/data-display/_internal/SRichInlineNode.vue +76 -0
  72. package/src/components/shared/data-display/_internal/SRichMath.css +42 -0
  73. package/src/components/shared/data-display/_internal/SRichMath.vue +102 -0
  74. package/src/components/shared/data-display/codeEditor.ts +200 -0
  75. package/src/components/shared/data-display/codeWorkspace.ts +233 -0
  76. package/src/components/shared/data-display/diff.ts +541 -0
  77. package/src/components/shared/data-display/richDocument.ts +876 -0
  78. package/src/components/shared/data-display/scrollViewport.ts +47 -0
  79. package/src/components/shared/data-display/toast.ts +194 -0
  80. package/src/components/shared/database/SDataGrid.vue +7 -4
  81. package/src/components/shared/database/SSqlEditor.vue +39 -39
  82. package/src/components/shared/feedback/SAsyncState.vue +3 -1
  83. package/src/components/shared/graph/SGraphViewport.vue +11 -9
  84. package/src/components/shared/navigation/SBreadcrumbs.vue +8 -5
  85. package/src/components/shared/navigation/SCatalogNavigator.vue +26 -20
  86. package/src/components/shared/navigation/STabList.vue +10 -6
  87. package/src/components/shared/navigation/STabs.vue +15 -11
  88. package/src/components/shared/navigation/SWizardSteps.vue +4 -2
  89. package/src/components/shared/persona/SPersonaProjectPicker.vue +31 -27
  90. package/src/components/shared/persona/SPersonaRunWorkbench.vue +39 -33
  91. package/src/components/shared/persona/_internal/SPersonaRunConsole.vue +8 -6
  92. package/src/components/shared/persona/_internal/labels.ts +66 -57
  93. package/src/components/shared/persona/presentation.ts +140 -110
  94. package/src/composables/useFloatingPosition.ts +39 -2
  95. package/src/composables/useUiKitLocale.ts +17 -0
  96. package/src/internal/codeHighlight.ts +110 -0
  97. package/src/internal/diffEngine.ts +464 -0
  98. package/src/internal/diffRuntime.ts +189 -0
  99. package/src/internal/feedbackPresentation.ts +33 -14
  100. package/src/internal/localeContext.ts +22 -0
  101. package/src/internal/ownedAttrs.ts +8 -0
  102. package/src/internal/runtimeContract.ts +11 -9
  103. package/src/internal/semanticSizing.ts +13 -0
  104. package/src/locales/catalog.ts +634 -0
  105. package/src/locales/locale.ts +472 -0
  106. package/src/locales/messages.generated.ts +1826 -0
  107. package/src/locales/messages.ts +150 -0
  108. package/src/stores/useNotifierStore.ts +61 -44
  109. package/src/styles/tokens.css +23 -2
  110. package/src/theme.ts +59 -32
  111. package/src/workers/diff.worker.ts +73 -0
  112. package/src/workers/diffProtocol.ts +155 -0
  113. package/src/components/shared/data-display/SToastContainer.css +0 -53
@@ -0,0 +1,47 @@
1
+ export type SScrollViewportAxis = 'x' | 'y' | 'both'
2
+ export type SScrollViewportOverscrollBehavior = 'auto' | 'contain' | 'none'
3
+ export type SScrollViewportPhysicalAxis = 'x' | 'y'
4
+ export type SScrollViewportBoundary = 'start' | 'end'
5
+ export type SScrollViewportBehavior = 'auto' | 'smooth'
6
+
7
+ export interface SScrollViewportPhysicalState {
8
+ /** Distance from the physical left edge, normalized across browser RTL scroll models. */
9
+ readonly x: number
10
+ /** Distance from the physical top edge. */
11
+ readonly y: number
12
+ readonly maxX: number
13
+ readonly maxY: number
14
+ }
15
+
16
+ export interface SScrollViewportSemanticState {
17
+ readonly inline: number
18
+ readonly block: number
19
+ readonly maxInline: number
20
+ readonly maxBlock: number
21
+ }
22
+
23
+ /** Serializable viewport position and logical boundaries. / Сериализуемая позиция viewport и логические границы. */
24
+ export interface SScrollViewportState {
25
+ readonly physical: SScrollViewportPhysicalState
26
+ readonly semantic: SScrollViewportSemanticState
27
+ readonly atInlineStart: boolean
28
+ readonly atInlineEnd: boolean
29
+ readonly atBlockStart: boolean
30
+ readonly atBlockEnd: boolean
31
+ }
32
+
33
+ export interface SScrollViewportRevealOptions {
34
+ readonly x?: number
35
+ readonly y?: number
36
+ readonly behavior?: SScrollViewportBehavior
37
+ }
38
+
39
+ /** Imperative viewport API without exposing its DOM owner. / Императивный API viewport без экспорта DOM-владельца. */
40
+ export interface SScrollViewportApi {
41
+ focus: () => void
42
+ scrollToBoundary: (
43
+ axis: SScrollViewportPhysicalAxis,
44
+ boundary: SScrollViewportBoundary,
45
+ ) => void
46
+ reveal: (options: SScrollViewportRevealOptions) => void
47
+ }
@@ -0,0 +1,194 @@
1
+ import type { SButtonAppearance } from '../controls/SButton.vue'
2
+ import type { FeedbackSeverity } from '../../../internal/feedbackPresentation'
3
+ import {
4
+ validateBoolean,
5
+ validateBoundedInteger,
6
+ validateExactString,
7
+ validateNonEmptyString,
8
+ } from '../../../internal/runtimeContract'
9
+
10
+ /** Serializable notification identity owned by the consumer. */
11
+ export type SToastId = string | number
12
+
13
+ /** Serializable action identity owned by the consumer. */
14
+ export type SToastActionId = string | number
15
+
16
+ /** Placement of a controlled toast viewport in the application viewport. */
17
+ export const S_TOAST_POSITIONS = [
18
+ 'top-start',
19
+ 'top-center',
20
+ 'top-end',
21
+ 'bottom-start',
22
+ 'bottom-center',
23
+ 'bottom-end',
24
+ ] as const
25
+ export type SToastPosition = (typeof S_TOAST_POSITIONS)[number]
26
+
27
+ /** Selects the owner of presentation lifetime without changing data ownership. */
28
+ export const S_TOAST_LIFETIME_OWNERS = ['consumer', 'viewport'] as const
29
+ export type SToastLifetimeOwner = (typeof S_TOAST_LIFETIME_OWNERS)[number]
30
+
31
+ /** Serializable action rendered by the shared toast presentation. */
32
+ export interface SToastAction<ActionId extends SToastActionId = string> {
33
+ readonly id: ActionId
34
+ readonly label: string
35
+ readonly appearance?: SButtonAppearance
36
+ }
37
+
38
+ /** Controlled notification record. The viewport never mutates this record or its source array. */
39
+ export interface SToastPresentation<
40
+ Id extends SToastId = string,
41
+ ActionId extends SToastActionId = string,
42
+ > {
43
+ readonly id: Id
44
+ readonly severity: FeedbackSeverity
45
+ readonly title?: string
46
+ readonly message: string
47
+ readonly actions?: readonly SToastAction<ActionId>[]
48
+ readonly dismissible?: boolean
49
+ /** Presentation lifetime in milliseconds. Omitted or zero means persistent. */
50
+ readonly durationMs?: number
51
+ }
52
+
53
+ export interface SToastDismissIntent<Id extends SToastId = string> {
54
+ readonly id: Id
55
+ readonly reason: 'close'
56
+ }
57
+
58
+ export interface SToastActionIntent<
59
+ Id extends SToastId = string,
60
+ ActionId extends SToastActionId = string,
61
+ > {
62
+ readonly notificationId: Id
63
+ readonly actionId: ActionId
64
+ }
65
+
66
+ export interface SToastLifetimeExpiredIntent<Id extends SToastId = string> {
67
+ readonly id: Id
68
+ }
69
+
70
+ const TOAST_SEVERITIES = ['info', 'success', 'warn', 'danger'] as const
71
+ const ACTION_APPEARANCES = ['solid', 'soft', 'outline', 'ghost', 'text'] as const
72
+ const TOAST_KEYS = new Set(['id', 'severity', 'title', 'message', 'actions', 'dismissible', 'durationMs'])
73
+ const ACTION_KEYS = new Set(['id', 'label', 'appearance'])
74
+
75
+ function assertRecord(owner: string, coordinate: string, value: unknown): asserts value is Record<string, unknown> {
76
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) return
77
+ throw new TypeError(
78
+ `${owner}: ${coordinate} должен быть object. / ${owner}: ${coordinate} must be an object.`,
79
+ )
80
+ }
81
+
82
+ function assertExactKeys(
83
+ owner: string,
84
+ coordinate: string,
85
+ value: Record<string, unknown>,
86
+ allowed: ReadonlySet<string>,
87
+ ): void {
88
+ const unknownKeys = Object.keys(value).filter((key) => !allowed.has(key))
89
+ if (unknownKeys.length === 0) return
90
+ throw new TypeError(
91
+ `${owner}: ${coordinate} содержит неизвестные поля ${unknownKeys.map((key) => JSON.stringify(key)).join(', ')}. `
92
+ + `/ ${owner}: ${coordinate} contains unknown fields ${unknownKeys.map((key) => JSON.stringify(key)).join(', ')}.`,
93
+ )
94
+ }
95
+
96
+ function validateIdentifier(owner: string, coordinate: string, value: unknown): SToastId {
97
+ if (typeof value === 'string') return validateNonEmptyString(owner, coordinate, value)
98
+ if (typeof value === 'number' && Number.isFinite(value)) return value
99
+ throw new TypeError(
100
+ `${owner}: ${coordinate} должен быть непустой строкой или конечным числом. `
101
+ + `/ ${owner}: ${coordinate} must be a non-empty string or finite number.`,
102
+ )
103
+ }
104
+
105
+ function validateAction(owner: string, coordinate: string, value: unknown): void {
106
+ assertRecord(owner, coordinate, value)
107
+ assertExactKeys(owner, coordinate, value, ACTION_KEYS)
108
+ validateIdentifier(owner, `${coordinate}.id`, value.id)
109
+ validateNonEmptyString(owner, `${coordinate}.label`, value.label)
110
+ if (value.appearance !== undefined) {
111
+ validateExactString(owner, `${coordinate}.appearance`, value.appearance, ACTION_APPEARANCES)
112
+ }
113
+ }
114
+
115
+ /** Runtime-validates a readonly controlled notification list and preserves its generic IDs. */
116
+ export function validateToastPresentations<
117
+ Id extends SToastId,
118
+ ActionId extends SToastActionId,
119
+ >(
120
+ owner: string,
121
+ value: readonly SToastPresentation<Id, ActionId>[],
122
+ ): readonly SToastPresentation<Id, ActionId>[] {
123
+ if (!Array.isArray(value)) {
124
+ throw new TypeError(`${owner}: notifications должен быть массивом. / ${owner}: notifications must be an array.`)
125
+ }
126
+ const notificationIds = new Set<SToastId>()
127
+ value.forEach((notification, notificationIndex) => {
128
+ const coordinate = `notifications[${notificationIndex}]`
129
+ assertRecord(owner, coordinate, notification)
130
+ assertExactKeys(owner, coordinate, notification, TOAST_KEYS)
131
+ const id = validateIdentifier(owner, `${coordinate}.id`, notification.id)
132
+ if (notificationIds.has(id)) {
133
+ throw new TypeError(
134
+ `${owner}: ${coordinate}.id дублирует ${JSON.stringify(id)}. `
135
+ + `/ ${owner}: ${coordinate}.id duplicates ${JSON.stringify(id)}.`,
136
+ )
137
+ }
138
+ notificationIds.add(id)
139
+ validateExactString(owner, `${coordinate}.severity`, notification.severity, TOAST_SEVERITIES)
140
+ if (notification.title !== undefined) {
141
+ validateNonEmptyString(owner, `${coordinate}.title`, notification.title)
142
+ }
143
+ if (typeof notification.message !== 'string') {
144
+ throw new TypeError(
145
+ `${owner}: ${coordinate}.message должен быть строкой. `
146
+ + `/ ${owner}: ${coordinate}.message must be a string.`,
147
+ )
148
+ }
149
+ if (notification.dismissible !== undefined) {
150
+ validateBoolean(owner, `${coordinate}.dismissible`, notification.dismissible)
151
+ }
152
+ if (notification.durationMs !== undefined) {
153
+ validateBoundedInteger(owner, `${coordinate}.durationMs`, notification.durationMs, {
154
+ minimum: 0,
155
+ maximum: 2_147_483_647,
156
+ })
157
+ }
158
+ if (notification.actions !== undefined) {
159
+ if (!Array.isArray(notification.actions)) {
160
+ throw new TypeError(
161
+ `${owner}: ${coordinate}.actions должен быть массивом. `
162
+ + `/ ${owner}: ${coordinate}.actions must be an array.`,
163
+ )
164
+ }
165
+ const actionIds = new Set<SToastId>()
166
+ notification.actions.forEach((action, actionIndex) => {
167
+ const actionCoordinate = `${coordinate}.actions[${actionIndex}]`
168
+ validateAction(owner, actionCoordinate, action)
169
+ const actionId = validateIdentifier(owner, `${actionCoordinate}.id`, action.id)
170
+ if (actionIds.has(actionId)) {
171
+ throw new TypeError(
172
+ `${owner}: ${actionCoordinate}.id дублирует ${JSON.stringify(actionId)}. `
173
+ + `/ ${owner}: ${actionCoordinate}.id duplicates ${JSON.stringify(actionId)}.`,
174
+ )
175
+ }
176
+ actionIds.add(actionId)
177
+ })
178
+ }
179
+ })
180
+ return value
181
+ }
182
+
183
+ export function validateToastPosition(value: unknown): SToastPosition {
184
+ return validateExactString('SControlledToastViewport', 'position', value, S_TOAST_POSITIONS)
185
+ }
186
+
187
+ export function validateToastLifetimeOwner(value: unknown): SToastLifetimeOwner {
188
+ return validateExactString(
189
+ 'SControlledToastViewport',
190
+ 'lifetimeOwner',
191
+ value,
192
+ S_TOAST_LIFETIME_OWNERS,
193
+ )
194
+ }
@@ -26,9 +26,9 @@
26
26
  :data="validatedGrid.rows"
27
27
  :get-row-key="getRowKey"
28
28
  :loading="validatedLoading"
29
- loading-message="Загрузка данных..."
29
+ :loading-message="t('table.dataGridLoading')"
30
30
  :error-message="errorMessage"
31
- :empty-message="emptyMessage"
31
+ :empty-message="resolvedEmptyMessage"
32
32
  :selected-row-key="selectedRowId"
33
33
  :max-height="validatedMaxHeight"
34
34
  density="compact"
@@ -127,6 +127,7 @@ export interface SDataGridProps<Key extends SDataGridColumnKey = SDataGridColumn
127
127
 
128
128
  <script setup lang="ts" generic="ColumnKey extends string = string">
129
129
  import { computed, reactive, useId, useSlots, watch } from 'vue'
130
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
130
131
  import { hasOwn } from '../../../internal/es2020'
131
132
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
132
133
  import {
@@ -170,13 +171,15 @@ const props = withDefaults(defineProps<SDataGridProps<ColumnKey>>(), {
170
171
  disabled: false,
171
172
  readOnly: false,
172
173
  errorMessage: '',
173
- emptyMessage: 'Нет строк для отображения',
174
+ emptyMessage: undefined,
174
175
  selectedRowId: undefined,
175
176
  maxHeight: 'none',
176
177
  presentation: 'panel',
177
178
  ariaLabel: undefined,
178
179
  ariaLabelledby: undefined,
179
180
  })
181
+ const { t } = useUiKitLocale()
182
+ const resolvedEmptyMessage = computed(() => props.emptyMessage ?? t('table.dataGridEmpty'))
180
183
 
181
184
  const ownedAttrs = useOwnedAttrs({ component: 'SDataGrid', owner: 'data grid adapter root' })
182
185
  const slots = useSlots()
@@ -334,7 +337,7 @@ const resolvedAccessibleName = computed(() => {
334
337
  }
335
338
  return {
336
339
  ariaLabel: props.ariaLabel === undefined
337
- ? 'Таблица данных'
340
+ ? t('table.accessibleName')
338
341
  : validateNonEmptyString('SDataGrid', 'ariaLabel', props.ariaLabel),
339
342
  ariaLabelledby: undefined,
340
343
  }
@@ -30,6 +30,7 @@
30
30
  :aria-describedby="hasStatus ? generatedStatusId : undefined"
31
31
  :invalid="statusHasError"
32
32
  :next-selection-pos="nextSelectionPos"
33
+ :diagnostics="codeEditorDiagnostics"
33
34
  @update:model-value="onUpdate"
34
35
  @cursor-activity="emitCursorActivity"
35
36
  @selection-applied="emit('selection-applied', $event)"
@@ -51,6 +52,7 @@
51
52
 
52
53
  <script setup lang="ts">
53
54
  import { computed, ref, useId, useSlots } from 'vue'
55
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
54
56
  import { hasOwn } from '../../../internal/es2020'
55
57
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
56
58
  import { provideTrustedCodeEditorExtensions } from '../../../internal/codeEditorExtensions'
@@ -63,8 +65,7 @@ import {
63
65
  validateNonEmptyString,
64
66
  } from '../../../internal/runtimeContract'
65
67
  import { autocompletion, type Completion, type CompletionContext } from '@codemirror/autocomplete'
66
- import { linter, type Diagnostic as CodeMirrorDiagnostic } from '@codemirror/lint'
67
- import { EditorState, type Extension } from '@codemirror/state'
68
+ import type { Extension } from '@codemirror/state'
68
69
  import SCodeEditor, {
69
70
  type SCodeEditorCapabilities,
70
71
  type SCodeEditorCursorActivity,
@@ -73,6 +74,7 @@ import SCodeEditor, {
73
74
  type SCodeEditorSelectionAppliedPayload,
74
75
  type SCodeEditorViewport,
75
76
  } from '../data-display/SCodeEditor.vue'
77
+ import type { SCodeEditorDiagnostic } from '../data-display/codeEditor'
76
78
  import SPanel from '../containers/SPanel.vue'
77
79
  import SStatus, { type StatusTone } from '../data-display/SStatus.vue'
78
80
 
@@ -119,7 +121,7 @@ export interface Props {
119
121
 
120
122
  const props = withDefaults(defineProps<Props>(), {
121
123
  title: undefined,
122
- placeholder: 'select * from public.table_name;',
124
+ placeholder: undefined,
123
125
  readOnly: false,
124
126
  disabled: false,
125
127
  loading: false,
@@ -134,6 +136,8 @@ const props = withDefaults(defineProps<Props>(), {
134
136
  ariaLabelledby: undefined,
135
137
  nextSelectionPos: null,
136
138
  })
139
+ const { t } = useUiKitLocale()
140
+ const resolvedPlaceholder = computed(() => props.placeholder ?? t('editor.sqlPlaceholder'))
137
141
 
138
142
  const ownedAttrs = useOwnedAttrs({ component: 'SSqlEditor', owner: 'SQL editor adapter root' })
139
143
  const slots = useSlots()
@@ -154,7 +158,11 @@ const validatedAutofocus = computed(() => validateBoolean('SSqlEditor', 'autofoc
154
158
  const validatedShowLineNumbers = computed(() => validateBoolean('SSqlEditor', 'showLineNumbers', props.showLineNumbers))
155
159
  const validatedViewport = computed(() => validateCodeEditorViewport('SSqlEditor', props.viewport))
156
160
  const validatedTitle = computed(() => props.title === undefined ? '' : validateNonEmptyString('SSqlEditor', 'title', props.title))
157
- const validatedPlaceholder = computed(() => validateNonEmptyString('SSqlEditor', 'placeholder', props.placeholder))
161
+ const validatedPlaceholder = computed(() => validateNonEmptyString(
162
+ 'SSqlEditor',
163
+ 'placeholder',
164
+ resolvedPlaceholder.value,
165
+ ))
158
166
  const validatedErrorMessage = computed(() => {
159
167
  if (typeof props.errorMessage !== 'string') {
160
168
  throw new TypeError('SSqlEditor: errorMessage должен быть строкой. / SSqlEditor: errorMessage must be a string.')
@@ -185,7 +193,7 @@ const resolvedAccessibleName = computed(() => {
185
193
  const generatedTitleId = `s-sql-editor-title-${useId()}`
186
194
  const generatedStatusId = `s-sql-editor-status-${useId()}`
187
195
  const panelState = computed(() => validatedLoading.value
188
- ? { status: 'loading' as const, message: 'Загрузка SQL editor...' }
196
+ ? { status: 'loading' as const, message: t('editor.sqlLoading') }
189
197
  : { status: 'ready' as const })
190
198
 
191
199
  const onUpdate = (value: string) => {
@@ -226,42 +234,27 @@ const completionType = (kind: SSqlEditorCompletionItem['kind']) => {
226
234
  }
227
235
  }
228
236
 
229
- const diagnosticsExtension = computed(() =>
230
- linter((view) =>
231
- validatedDiagnostics.value.map((diagnostic): CodeMirrorDiagnostic => {
232
- const { from, to } = resolveDiagnosticRange(view.state.doc, diagnostic)
233
- return {
234
- from,
235
- to,
236
- severity: diagnosticSeverity(diagnostic.severity),
237
- message: diagnostic.message,
238
- }
239
- }),
240
- ),
241
- )
242
-
243
237
  const resolveDiagnosticRange = (
244
- doc: EditorState['doc'],
238
+ source: string,
245
239
  diagnostic: SSqlEditorDiagnostic,
246
240
  ) : { from: number; to: number } => {
247
241
  if ('from' in diagnostic) {
248
- const from = validateBoundedInteger('SSqlEditor', 'diagnostics[].from', diagnostic.from, { minimum: 0, maximum: doc.length })
242
+ const from = validateBoundedInteger('SSqlEditor', 'diagnostics[].from', diagnostic.from, { minimum: 0, maximum: source.length })
249
243
  const to = diagnostic.to === undefined
250
- ? Math.min(from + 1, doc.length)
251
- : validateBoundedInteger('SSqlEditor', 'diagnostics[].to', diagnostic.to, { minimum: from, maximum: doc.length })
244
+ ? Math.min(from + 1, source.length)
245
+ : validateBoundedInteger('SSqlEditor', 'diagnostics[].to', diagnostic.to, { minimum: from, maximum: source.length })
252
246
  return { from, to }
253
247
  }
254
- const lineNumber = validateBoundedInteger('SSqlEditor', 'diagnostics[].line', diagnostic.line, { minimum: 1, maximum: doc.lines })
255
- const line = doc.line(lineNumber)
256
- const column = validateBoundedInteger('SSqlEditor', 'diagnostics[].column', diagnostic.column, { minimum: 1, maximum: line.length + 1 })
257
- const from = line.from + column - 1
258
- return { from, to: Math.min(from + 1, doc.length) }
259
- }
260
-
261
- const diagnosticSeverity = (severity: SSqlEditorDiagnostic['severity']): CodeMirrorDiagnostic['severity'] => {
262
- if (severity === 'info') return 'info'
263
- if (severity === 'warning') return 'warning'
264
- return 'error'
248
+ const lines = source.split('\n')
249
+ const lineNumber = validateBoundedInteger('SSqlEditor', 'diagnostics[].line', diagnostic.line, { minimum: 1, maximum: lines.length })
250
+ const lineText = lines[lineNumber - 1]
251
+ if (lineText === undefined) {
252
+ throw new RangeError(`SSqlEditor: diagnostics[].line ${lineNumber} отсутствует в документе. / SSqlEditor: diagnostics[].line ${lineNumber} is missing from the document.`)
253
+ }
254
+ const column = validateBoundedInteger('SSqlEditor', 'diagnostics[].column', diagnostic.column, { minimum: 1, maximum: lineText.length + 1 })
255
+ const lineFrom = lines.slice(0, lineNumber - 1).reduce((offset, line) => offset + line.length + 1, 0)
256
+ const from = lineFrom + column - 1
257
+ return { from, to: Math.min(from + 1, source.length) }
265
258
  }
266
259
 
267
260
  function assertPlainExactRecord(owner: string, value: unknown, allowed: readonly string[]): Record<string, unknown> {
@@ -312,7 +305,17 @@ const validatedDiagnostics = computed<SSqlEditorDiagnostic[]>(() => {
312
305
  return { ...common, line: record.line as number, column: record.column as number }
313
306
  })
314
307
  })
315
- const statusLabel = computed(() => validatedErrorMessage.value || `${validatedDiagnostics.value.length} диагностик`)
308
+ const codeEditorDiagnostics = computed<readonly SCodeEditorDiagnostic<string>[]>(() =>
309
+ validatedDiagnostics.value.map((diagnostic, index) => ({
310
+ id: `sql-diagnostic-${index}`,
311
+ severity: diagnostic.severity,
312
+ message: diagnostic.message,
313
+ ...resolveDiagnosticRange(props.modelValue, diagnostic),
314
+ source: 'SSqlEditor',
315
+ })))
316
+ const statusLabel = computed(() => validatedErrorMessage.value || t('editor.diagnosticsCount', {
317
+ count: validatedDiagnostics.value.length,
318
+ }))
316
319
  const hasStatus = computed(() => (
317
320
  validatedErrorMessage.value !== ''
318
321
  || validatedDiagnostics.value.length > 0
@@ -336,10 +339,7 @@ const editorCapabilities = computed<SCodeEditorCapabilities>(() => ({
336
339
  }))
337
340
 
338
341
  const extensions = computed<readonly Extension[]>(() => {
339
- const result: Extension[] = []
340
- result.push(autocompletion({ override: [completionSource] }))
341
- result.push(diagnosticsExtension.value)
342
- return result
342
+ return [autocompletion({ override: [completionSource] })]
343
343
  })
344
344
 
345
345
  provideTrustedCodeEditorExtensions('SSqlEditor', extensions)
@@ -38,6 +38,7 @@
38
38
 
39
39
  <script setup lang="ts">
40
40
  import { Fragment, computed, h, type Slots, type VNodeChild } from 'vue'
41
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
41
42
 
42
43
  import { resolveAsyncFeedbackPresentation } from '../../../internal/feedbackPresentation'
43
44
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
@@ -75,6 +76,7 @@ const slots = defineSlots<{
75
76
  empty?: (props: { message: string; state: Extract<SAsyncStateContract, { status: 'empty' }> }) => VNodeChild
76
77
  error?: (props: { message: string; state: Extract<SAsyncStateContract, { status: 'error' }> }) => VNodeChild
77
78
  }>()
79
+ const { t } = useUiKitLocale()
78
80
 
79
81
  const ownedAttrs = useOwnedAttrs({
80
82
  component: 'SAsyncState',
@@ -148,7 +150,7 @@ const assertReadyAttrs = (): boolean => {
148
150
  }
149
151
  const statePresentation = computed(() => {
150
152
  const state = validatedState.value
151
- return state.status === 'ready' ? undefined : resolveAsyncFeedbackPresentation(state.status)
153
+ return state.status === 'ready' ? undefined : resolveAsyncFeedbackPresentation(state.status, t)
152
154
  })
153
155
  const resolvedMessage = computed(() => {
154
156
  const state = validatedState.value
@@ -56,6 +56,7 @@ import {
56
56
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
57
57
  import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
58
58
  import { useRuntimeTheme } from '../../../internal/runtimeTheme'
59
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
59
60
  import SListbox, { type SListboxOptionKeydownPayload } from '../controls/SListbox.vue'
60
61
  import SAsyncState from '../feedback/SAsyncState.vue'
61
62
 
@@ -103,6 +104,7 @@ const emit = defineEmits<{
103
104
  }>()
104
105
 
105
106
  const ownedAttrs = useOwnedAttrs({ component: 'SGraphViewport', owner: 'graph viewport root' })
107
+ const { t } = useUiKitLocale()
106
108
  useInteractiveLeafRegistration({ owner: 'SGraphViewport' })
107
109
  const rendererRef = ref<HTMLElement | null>(null)
108
110
  const runtimeTheme = useRuntimeTheme()
@@ -394,7 +396,7 @@ function refreshRuntimeTheme(): void {
394
396
  } catch (error) {
395
397
  failRender(
396
398
  'sigma_mount_failed',
397
- error instanceof Error ? error.message : 'SGraphViewport: не удалось применить semantic runtime theme.',
399
+ error instanceof Error ? error.message : t('graph.themeFailed'),
398
400
  )
399
401
  }
400
402
  }
@@ -409,12 +411,12 @@ function mountSigma(): void {
409
411
  } catch (error) {
410
412
  failRender(
411
413
  error instanceof SGraphContractError ? 'invalid_graph' : 'sigma_mount_failed',
412
- error instanceof Error ? error.message : 'Граф содержит некорректные данные.',
414
+ error instanceof Error ? error.message : t('graph.invalidData'),
413
415
  )
414
416
  return
415
417
  }
416
418
  if (!hasWebgl2Support()) {
417
- failRender('webgl_unavailable', 'Графический движок требует WebGL2.')
419
+ failRender('webgl_unavailable', t('graph.webglRequired'))
418
420
  return
419
421
  }
420
422
  const startedAt = performance.now()
@@ -428,7 +430,7 @@ function mountSigma(): void {
428
430
  } catch (error) {
429
431
  failRender(
430
432
  error instanceof SGraphContractError ? 'invalid_graph' : 'sigma_mount_failed',
431
- error instanceof Error ? error.message : 'Графический движок не смог построить карту.',
433
+ error instanceof Error ? error.message : t('graph.renderFailed'),
432
434
  )
433
435
  }
434
436
  }
@@ -493,7 +495,7 @@ function replaceGraph(): void {
493
495
  assertViewportConfiguration()
494
496
  nextGraph = buildGraph(props.graph)
495
497
  } catch (error) {
496
- failRender('invalid_graph', error instanceof Error ? error.message : 'Граф содержит некорректные данные.')
498
+ failRender('invalid_graph', error instanceof Error ? error.message : t('graph.invalidData'))
497
499
  return
498
500
  }
499
501
  try {
@@ -511,7 +513,7 @@ function replaceGraph(): void {
511
513
  } catch (error) {
512
514
  failRender(
513
515
  error instanceof SGraphContractError ? 'invalid_graph' : 'sigma_mount_failed',
514
- error instanceof Error ? error.message : 'SGraphViewport: не удалось заменить граф.',
516
+ error instanceof Error ? error.message : t('graph.renderFailed'),
515
517
  )
516
518
  }
517
519
  }
@@ -520,7 +522,7 @@ function refreshValidatedInteraction(): void {
520
522
  try {
521
523
  assertViewportConfiguration()
522
524
  } catch (error) {
523
- failRender('invalid_graph', error instanceof Error ? error.message : 'SGraphViewport: interaction некорректен.')
525
+ failRender('invalid_graph', error instanceof Error ? error.message : t('graph.invalidData'))
524
526
  return
525
527
  }
526
528
  if (resolvedPositioningMode.value === 'readonly') cancelNodeDrag()
@@ -535,7 +537,7 @@ function refreshValidatedSettings(): void {
535
537
  try {
536
538
  assertViewportConfiguration()
537
539
  } catch (error) {
538
- failRender('invalid_graph', error instanceof Error ? error.message : 'SGraphViewport: renderer settings некорректны.')
540
+ failRender('invalid_graph', error instanceof Error ? error.message : t('graph.invalidData'))
539
541
  return
540
542
  }
541
543
  if (!sigma) {
@@ -548,7 +550,7 @@ function refreshValidatedSettings(): void {
548
550
  } catch (error) {
549
551
  failRender(
550
552
  'sigma_mount_failed',
551
- error instanceof Error ? error.message : 'SGraphViewport: не удалось обновить renderer settings.',
553
+ error instanceof Error ? error.message : t('graph.renderFailed'),
552
554
  )
553
555
  }
554
556
  }
@@ -1,5 +1,6 @@
1
1
  <script setup lang="ts">
2
- import { watch } from 'vue'
2
+ import { computed, watch } from 'vue'
3
+ import { useUiKitLocale } from '../../../composables/useUiKitLocale'
3
4
 
4
5
  import { resolveLinkTarget } from '../../../internal/linkTarget'
5
6
  import { useOwnedAttrs } from '../../../internal/ownedAttrs'
@@ -39,8 +40,10 @@ const props = withDefaults(defineProps<{
39
40
  items: readonly BreadcrumbItem[]
40
41
  ariaLabel?: string
41
42
  }>(), {
42
- ariaLabel: 'Навигационная цепочка',
43
+ ariaLabel: undefined,
43
44
  })
45
+ const { t } = useUiKitLocale()
46
+ const resolvedAriaLabel = computed(() => props.ariaLabel ?? t('navigation.breadcrumbs'))
44
47
 
45
48
  const emit = defineEmits<{
46
49
  action: [id: string, event: MouseEvent]
@@ -60,7 +63,7 @@ function assertRecord(value: unknown, coordinate: string): asserts value is Reco
60
63
  }
61
64
 
62
65
  function validateContract(): void {
63
- if (typeof props.ariaLabel !== 'string' || !props.ariaLabel.trim()) {
66
+ if (!resolvedAriaLabel.value.trim()) {
64
67
  throw new TypeError('SBreadcrumbs: ariaLabel must be a non-empty string')
65
68
  }
66
69
  if (!Array.isArray(props.items) || props.items.length === 0) {
@@ -114,11 +117,11 @@ function validateContract(): void {
114
117
  }
115
118
  }
116
119
 
117
- watch(() => [props.items, props.ariaLabel] as const, validateContract, { immediate: true, deep: true })
120
+ watch(() => [props.items, resolvedAriaLabel.value] as const, validateContract, { immediate: true, deep: true })
118
121
  </script>
119
122
 
120
123
  <template>
121
- <nav v-bind="ownedAttrs.bindings()" class="s-breadcrumbs" :aria-label="ariaLabel">
124
+ <nav v-bind="ownedAttrs.bindings()" class="s-breadcrumbs" :aria-label="resolvedAriaLabel">
122
125
  <ol class="s-breadcrumbs__list">
123
126
  <li v-for="(item, index) in items" :key="item.id" class="s-breadcrumbs__item">
124
127
  <ChevronRight v-if="index > 0" class="s-breadcrumbs__divider" aria-hidden="true" />