@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.
- package/README.md +188 -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/package.json +103 -50
- package/src/components/layout/SAppShell.vue +3 -1
- package/src/components/layout/SDockRegion.vue +5 -3
- package/src/components/layout/SWorkbenchPanelShell.vue +4 -2
- package/src/components/shared/_internal/SDialogSurface.css +5 -0
- package/src/components/shared/_internal/SDialogSurface.vue +3 -1
- 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/complex/SKanbanLane.vue +4 -2
- package/src/components/shared/containers/SCloudChromeRow.vue +3 -2
- package/src/components/shared/containers/SDrawer.vue +5 -2
- 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/_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 +42 -20
- 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/_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 +20 -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,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="
|
|
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
|
|
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:
|
|
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(
|
|
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: '
|
|
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
|
-
|
|
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:
|
|
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,
|
|
251
|
-
: validateBoundedInteger('SSqlEditor', 'diagnostics[].to', diagnostic.to, { minimum: from, maximum:
|
|
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
|
|
255
|
-
const
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
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
|
|
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
|
-
|
|
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 : '
|
|
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', '
|
|
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 : '
|
|
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 : '
|
|
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 : '
|
|
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 : '
|
|
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 (
|
|
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,
|
|
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="
|
|
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" />
|